PageRenderTime 184ms CodeModel.GetById 48ms app.highlight 60ms RepoModel.GetById 65ms app.codeStats 0ms

/wp-admin/js/theme.js

https://gitlab.com/spot-software/company-website
JavaScript | 1781 lines | 1077 code | 369 blank | 335 comment | 135 complexity | 3715b37b10edab644542406f0caca236 MD5 | raw file
   1/* global _wpThemeSettings, confirm */
   2window.wp = window.wp || {};
   3
   4( function($) {
   5
   6// Set up our namespace...
   7var themes, l10n;
   8themes = wp.themes = wp.themes || {};
   9
  10// Store the theme data and settings for organized and quick access
  11// themes.data.settings, themes.data.themes, themes.data.l10n
  12themes.data = _wpThemeSettings;
  13l10n = themes.data.l10n;
  14
  15// Shortcut for isInstall check
  16themes.isInstall = !! themes.data.settings.isInstall;
  17
  18// Setup app structure
  19_.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
  20
  21themes.Model = Backbone.Model.extend({
  22	// Adds attributes to the default data coming through the .org themes api
  23	// Map `id` to `slug` for shared code
  24	initialize: function() {
  25		var description;
  26
  27		// If theme is already installed, set an attribute.
  28		if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
  29			this.set({ installed: true });
  30		}
  31
  32		// Set the attributes
  33		this.set({
  34			// slug is for installation, id is for existing.
  35			id: this.get( 'slug' ) || this.get( 'id' )
  36		});
  37
  38		// Map `section.description` to `description`
  39		// as the API sometimes returns it differently
  40		if ( this.has( 'sections' ) ) {
  41			description = this.get( 'sections' ).description;
  42			this.set({ description: description });
  43		}
  44	}
  45});
  46
  47// Main view controller for themes.php
  48// Unifies and renders all available views
  49themes.view.Appearance = wp.Backbone.View.extend({
  50
  51	el: '#wpbody-content .wrap .theme-browser',
  52
  53	window: $( window ),
  54	// Pagination instance
  55	page: 0,
  56
  57	// Sets up a throttler for binding to 'scroll'
  58	initialize: function( options ) {
  59		// Scroller checks how far the scroll position is
  60		_.bindAll( this, 'scroller' );
  61
  62		this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
  63		// Bind to the scroll event and throttle
  64		// the results from this.scroller
  65		this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) );
  66	},
  67
  68	// Main render control
  69	render: function() {
  70		// Setup the main theme view
  71		// with the current theme collection
  72		this.view = new themes.view.Themes({
  73			collection: this.collection,
  74			parent: this
  75		});
  76
  77		// Render search form.
  78		this.search();
  79
  80		// Render and append
  81		this.view.render();
  82		this.$el.empty().append( this.view.el ).addClass( 'rendered' );
  83		this.$el.append( '<br class="clear"/>' );
  84	},
  85
  86	// Defines search element container
  87	searchContainer: $( '#wpbody h1:first' ),
  88
  89	// Search input and view
  90	// for current theme collection
  91	search: function() {
  92		var view,
  93			self = this;
  94
  95		// Don't render the search if there is only one theme
  96		if ( themes.data.themes.length === 1 ) {
  97			return;
  98		}
  99
 100		view = new this.SearchView({
 101			collection: self.collection,
 102			parent: this
 103		});
 104
 105		// Render and append after screen title
 106		view.render();
 107		this.searchContainer
 108			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
 109			.append( view.el );
 110	},
 111
 112	// Checks when the user gets close to the bottom
 113	// of the mage and triggers a theme:scroll event
 114	scroller: function() {
 115		var self = this,
 116			bottom, threshold;
 117
 118		bottom = this.window.scrollTop() + self.window.height();
 119		threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
 120		threshold = Math.round( threshold * 0.9 );
 121
 122		if ( bottom > threshold ) {
 123			this.trigger( 'theme:scroll' );
 124		}
 125	}
 126});
 127
 128// Set up the Collection for our theme data
 129// @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
 130themes.Collection = Backbone.Collection.extend({
 131
 132	model: themes.Model,
 133
 134	// Search terms
 135	terms: '',
 136
 137	// Controls searching on the current theme collection
 138	// and triggers an update event
 139	doSearch: function( value ) {
 140
 141		// Don't do anything if we've already done this search
 142		// Useful because the Search handler fires multiple times per keystroke
 143		if ( this.terms === value ) {
 144			return;
 145		}
 146
 147		// Updates terms with the value passed
 148		this.terms = value;
 149
 150		// If we have terms, run a search...
 151		if ( this.terms.length > 0 ) {
 152			this.search( this.terms );
 153		}
 154
 155		// If search is blank, show all themes
 156		// Useful for resetting the views when you clean the input
 157		if ( this.terms === '' ) {
 158			this.reset( themes.data.themes );
 159			$( 'body' ).removeClass( 'no-results' );
 160		}
 161
 162		// Trigger an 'update' event
 163		this.trigger( 'update' );
 164	},
 165
 166	// Performs a search within the collection
 167	// @uses RegExp
 168	search: function( term ) {
 169		var match, results, haystack, name, description, author;
 170
 171		// Start with a full collection
 172		this.reset( themes.data.themes, { silent: true } );
 173
 174		// Escape the term string for RegExp meta characters
 175		term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
 176
 177		// Consider spaces as word delimiters and match the whole string
 178		// so matching terms can be combined
 179		term = term.replace( / /g, ')(?=.*' );
 180		match = new RegExp( '^(?=.*' + term + ').+', 'i' );
 181
 182		// Find results
 183		// _.filter and .test
 184		results = this.filter( function( data ) {
 185			name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
 186			description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
 187			author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
 188
 189			haystack = _.union( name, data.get( 'id' ), description, author, data.get( 'tags' ) );
 190
 191			if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
 192				data.set( 'displayAuthor', true );
 193			}
 194
 195			return match.test( haystack );
 196		});
 197
 198		if ( results.length === 0 ) {
 199			this.trigger( 'query:empty' );
 200		} else {
 201			$( 'body' ).removeClass( 'no-results' );
 202		}
 203
 204		this.reset( results );
 205	},
 206
 207	// Paginates the collection with a helper method
 208	// that slices the collection
 209	paginate: function( instance ) {
 210		var collection = this;
 211		instance = instance || 0;
 212
 213		// Themes per instance are set at 20
 214		collection = _( collection.rest( 20 * instance ) );
 215		collection = _( collection.first( 20 ) );
 216
 217		return collection;
 218	},
 219
 220	count: false,
 221
 222	// Handles requests for more themes
 223	// and caches results
 224	//
 225	// When we are missing a cache object we fire an apiCall()
 226	// which triggers events of `query:success` or `query:fail`
 227	query: function( request ) {
 228		/**
 229		 * @static
 230		 * @type Array
 231		 */
 232		var queries = this.queries,
 233			self = this,
 234			query, isPaginated, count;
 235
 236		// Store current query request args
 237		// for later use with the event `theme:end`
 238		this.currentQuery.request = request;
 239
 240		// Search the query cache for matches.
 241		query = _.find( queries, function( query ) {
 242			return _.isEqual( query.request, request );
 243		});
 244
 245		// If the request matches the stored currentQuery.request
 246		// it means we have a paginated request.
 247		isPaginated = _.has( request, 'page' );
 248
 249		// Reset the internal api page counter for non paginated queries.
 250		if ( ! isPaginated ) {
 251			this.currentQuery.page = 1;
 252		}
 253
 254		// Otherwise, send a new API call and add it to the cache.
 255		if ( ! query && ! isPaginated ) {
 256			query = this.apiCall( request ).done( function( data ) {
 257
 258				// Update the collection with the queried data.
 259				if ( data.themes ) {
 260					self.reset( data.themes );
 261					count = data.info.results;
 262					// Store the results and the query request
 263					queries.push( { themes: data.themes, request: request, total: count } );
 264				}
 265
 266				// Trigger a collection refresh event
 267				// and a `query:success` event with a `count` argument.
 268				self.trigger( 'update' );
 269				self.trigger( 'query:success', count );
 270
 271				if ( data.themes && data.themes.length === 0 ) {
 272					self.trigger( 'query:empty' );
 273				}
 274
 275			}).fail( function() {
 276				self.trigger( 'query:fail' );
 277			});
 278		} else {
 279			// If it's a paginated request we need to fetch more themes...
 280			if ( isPaginated ) {
 281				return this.apiCall( request, isPaginated ).done( function( data ) {
 282					// Add the new themes to the current collection
 283					// @todo update counter
 284					self.add( data.themes );
 285					self.trigger( 'query:success' );
 286
 287					// We are done loading themes for now.
 288					self.loadingThemes = false;
 289
 290				}).fail( function() {
 291					self.trigger( 'query:fail' );
 292				});
 293			}
 294
 295			if ( query.themes.length === 0 ) {
 296				self.trigger( 'query:empty' );
 297			} else {
 298				$( 'body' ).removeClass( 'no-results' );
 299			}
 300
 301			// Only trigger an update event since we already have the themes
 302			// on our cached object
 303			if ( _.isNumber( query.total ) ) {
 304				this.count = query.total;
 305			}
 306
 307			this.reset( query.themes );
 308			if ( ! query.total ) {
 309				this.count = this.length;
 310			}
 311
 312			this.trigger( 'update' );
 313			this.trigger( 'query:success', this.count );
 314		}
 315	},
 316
 317	// Local cache array for API queries
 318	queries: [],
 319
 320	// Keep track of current query so we can handle pagination
 321	currentQuery: {
 322		page: 1,
 323		request: {}
 324	},
 325
 326	// Send request to api.wordpress.org/themes
 327	apiCall: function( request, paginated ) {
 328		return wp.ajax.send( 'query-themes', {
 329			data: {
 330			// Request data
 331				request: _.extend({
 332					per_page: 100,
 333					fields: {
 334						description: true,
 335						tested: true,
 336						requires: true,
 337						rating: true,
 338						downloaded: true,
 339						downloadLink: true,
 340						last_updated: true,
 341						homepage: true,
 342						num_ratings: true
 343					}
 344				}, request)
 345			},
 346
 347			beforeSend: function() {
 348				if ( ! paginated ) {
 349					// Spin it
 350					$( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
 351				}
 352			}
 353		});
 354	},
 355
 356	// Static status controller for when we are loading themes.
 357	loadingThemes: false
 358});
 359
 360// This is the view that controls each theme item
 361// that will be displayed on the screen
 362themes.view.Theme = wp.Backbone.View.extend({
 363
 364	// Wrap theme data on a div.theme element
 365	className: 'theme',
 366
 367	// Reflects which theme view we have
 368	// 'grid' (default) or 'detail'
 369	state: 'grid',
 370
 371	// The HTML template for each element to be rendered
 372	html: themes.template( 'theme' ),
 373
 374	events: {
 375		'click': themes.isInstall ? 'preview': 'expand',
 376		'keydown': themes.isInstall ? 'preview': 'expand',
 377		'touchend': themes.isInstall ? 'preview': 'expand',
 378		'keyup': 'addFocus',
 379		'touchmove': 'preventExpand'
 380	},
 381
 382	touchDrag: false,
 383
 384	render: function() {
 385		var data = this.model.toJSON();
 386		// Render themes using the html template
 387		this.$el.html( this.html( data ) ).attr({
 388			tabindex: 0,
 389			'aria-describedby' : data.id + '-action ' + data.id + '-name'
 390		});
 391
 392		// Renders active theme styles
 393		this.activeTheme();
 394
 395		if ( this.model.get( 'displayAuthor' ) ) {
 396			this.$el.addClass( 'display-author' );
 397		}
 398
 399		if ( this.model.get( 'installed' ) ) {
 400			this.$el.addClass( 'is-installed' );
 401		}
 402	},
 403
 404	// Adds a class to the currently active theme
 405	// and to the overlay in detailed view mode
 406	activeTheme: function() {
 407		if ( this.model.get( 'active' ) ) {
 408			this.$el.addClass( 'active' );
 409		}
 410	},
 411
 412	// Add class of focus to the theme we are focused on.
 413	addFocus: function() {
 414		var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
 415
 416		$('.theme.focus').removeClass('focus');
 417		$themeToFocus.addClass('focus');
 418	},
 419
 420	// Single theme overlay screen
 421	// It's shown when clicking a theme
 422	expand: function( event ) {
 423		var self = this;
 424
 425		event = event || window.event;
 426
 427		// 'enter' and 'space' keys expand the details view when a theme is :focused
 428		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
 429			return;
 430		}
 431
 432		// Bail if the user scrolled on a touch device
 433		if ( this.touchDrag === true ) {
 434			return this.touchDrag = false;
 435		}
 436
 437		// Prevent the modal from showing when the user clicks
 438		// one of the direct action buttons
 439		if ( $( event.target ).is( '.theme-actions a' ) ) {
 440			return;
 441		}
 442
 443		// Set focused theme to current element
 444		themes.focusedTheme = this.$el;
 445
 446		this.trigger( 'theme:expand', self.model.cid );
 447	},
 448
 449	preventExpand: function() {
 450		this.touchDrag = true;
 451	},
 452
 453	preview: function( event ) {
 454		var self = this,
 455			current, preview;
 456
 457		event = event || window.event;
 458
 459		// Bail if the user scrolled on a touch device
 460		if ( this.touchDrag === true ) {
 461			return this.touchDrag = false;
 462		}
 463
 464		// Allow direct link path to installing a theme.
 465		if ( $( event.target ).hasClass( 'button-primary' ) ) {
 466			return;
 467		}
 468
 469		// 'enter' and 'space' keys expand the details view when a theme is :focused
 470		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
 471			return;
 472		}
 473
 474		// pressing enter while focused on the buttons shouldn't open the preview
 475		if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
 476			return;
 477		}
 478
 479		event.preventDefault();
 480
 481		event = event || window.event;
 482
 483		// Set focus to current theme.
 484		themes.focusedTheme = this.$el;
 485
 486		// Construct a new Preview view.
 487		preview = new themes.view.Preview({
 488			model: this.model
 489		});
 490
 491		// Render the view and append it.
 492		preview.render();
 493		this.setNavButtonsState();
 494
 495		// Hide previous/next navigation if there is only one theme
 496		if ( this.model.collection.length === 1 ) {
 497			preview.$el.addClass( 'no-navigation' );
 498		} else {
 499			preview.$el.removeClass( 'no-navigation' );
 500		}
 501
 502		// Append preview
 503		$( 'div.wrap' ).append( preview.el );
 504
 505		// Listen to our preview object
 506		// for `theme:next` and `theme:previous` events.
 507		this.listenTo( preview, 'theme:next', function() {
 508
 509			// Keep local track of current theme model.
 510			current = self.model;
 511
 512			// If we have ventured away from current model update the current model position.
 513			if ( ! _.isUndefined( self.current ) ) {
 514				current = self.current;
 515			}
 516
 517			// Get next theme model.
 518			self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
 519
 520			// If we have no more themes, bail.
 521			if ( _.isUndefined( self.current ) ) {
 522				self.options.parent.parent.trigger( 'theme:end' );
 523				return self.current = current;
 524			}
 525
 526			preview.model = self.current;
 527
 528			// Render and append.
 529			preview.render();
 530			this.setNavButtonsState();
 531			$( '.next-theme' ).focus();
 532		})
 533		.listenTo( preview, 'theme:previous', function() {
 534
 535			// Keep track of current theme model.
 536			current = self.model;
 537
 538			// Bail early if we are at the beginning of the collection
 539			if ( self.model.collection.indexOf( self.current ) === 0 ) {
 540				return;
 541			}
 542
 543			// If we have ventured away from current model update the current model position.
 544			if ( ! _.isUndefined( self.current ) ) {
 545				current = self.current;
 546			}
 547
 548			// Get previous theme model.
 549			self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
 550
 551			// If we have no more themes, bail.
 552			if ( _.isUndefined( self.current ) ) {
 553				return;
 554			}
 555
 556			preview.model = self.current;
 557
 558			// Render and append.
 559			preview.render();
 560			this.setNavButtonsState();
 561			$( '.previous-theme' ).focus();
 562		});
 563
 564		this.listenTo( preview, 'preview:close', function() {
 565			self.current = self.model;
 566		});
 567	},
 568
 569	// Handles .disabled classes for previous/next buttons in theme installer preview
 570	setNavButtonsState: function() {
 571		var $themeInstaller = $( '.theme-install-overlay' ),
 572			current = _.isUndefined( this.current ) ? this.model : this.current;
 573
 574		// Disable previous at the zero position
 575		if ( 0 === this.model.collection.indexOf( current ) ) {
 576			$themeInstaller.find( '.previous-theme' ).addClass( 'disabled' );
 577		}
 578
 579		// Disable next if the next model is undefined
 580		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
 581			$themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
 582		}
 583	}
 584});
 585
 586// Theme Details view
 587// Set ups a modal overlay with the expanded theme data
 588themes.view.Details = wp.Backbone.View.extend({
 589
 590	// Wrap theme data on a div.theme element
 591	className: 'theme-overlay',
 592
 593	events: {
 594		'click': 'collapse',
 595		'click .delete-theme': 'deleteTheme',
 596		'click .left': 'previousTheme',
 597		'click .right': 'nextTheme'
 598	},
 599
 600	// The HTML template for the theme overlay
 601	html: themes.template( 'theme-single' ),
 602
 603	render: function() {
 604		var data = this.model.toJSON();
 605		this.$el.html( this.html( data ) );
 606		// Renders active theme styles
 607		this.activeTheme();
 608		// Set up navigation events
 609		this.navigation();
 610		// Checks screenshot size
 611		this.screenshotCheck( this.$el );
 612		// Contain "tabbing" inside the overlay
 613		this.containFocus( this.$el );
 614	},
 615
 616	// Adds a class to the currently active theme
 617	// and to the overlay in detailed view mode
 618	activeTheme: function() {
 619		// Check the model has the active property
 620		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
 621	},
 622
 623	// Keeps :focus within the theme details elements
 624	containFocus: function( $el ) {
 625		var $target;
 626
 627		// Move focus to the primary action
 628		_.delay( function() {
 629			$( '.theme-wrap a.button-primary:visible' ).focus();
 630		}, 500 );
 631
 632		$el.on( 'keydown.wp-themes', function( event ) {
 633
 634			// Tab key
 635			if ( event.which === 9 ) {
 636				$target = $( event.target );
 637
 638				// Keep focus within the overlay by making the last link on theme actions
 639				// switch focus to button.left on tabbing and vice versa
 640				if ( $target.is( 'button.left' ) && event.shiftKey ) {
 641					$el.find( '.theme-actions a:last-child' ).focus();
 642					event.preventDefault();
 643				} else if ( $target.is( '.theme-actions a:last-child' ) ) {
 644					$el.find( 'button.left' ).focus();
 645					event.preventDefault();
 646				}
 647			}
 648		});
 649	},
 650
 651	// Single theme overlay screen
 652	// It's shown when clicking a theme
 653	collapse: function( event ) {
 654		var self = this,
 655			scroll;
 656
 657		event = event || window.event;
 658
 659		// Prevent collapsing detailed view when there is only one theme available
 660		if ( themes.data.themes.length === 1 ) {
 661			return;
 662		}
 663
 664		// Detect if the click is inside the overlay
 665		// and don't close it unless the target was
 666		// the div.back button
 667		if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
 668
 669			// Add a temporary closing class while overlay fades out
 670			$( 'body' ).addClass( 'closing-overlay' );
 671
 672			// With a quick fade out animation
 673			this.$el.fadeOut( 130, function() {
 674				// Clicking outside the modal box closes the overlay
 675				$( 'body' ).removeClass( 'closing-overlay' );
 676				// Handle event cleanup
 677				self.closeOverlay();
 678
 679				// Get scroll position to avoid jumping to the top
 680				scroll = document.body.scrollTop;
 681
 682				// Clean the url structure
 683				themes.router.navigate( themes.router.baseUrl( '' ) );
 684
 685				// Restore scroll position
 686				document.body.scrollTop = scroll;
 687
 688				// Return focus to the theme div
 689				if ( themes.focusedTheme ) {
 690					themes.focusedTheme.focus();
 691				}
 692			});
 693		}
 694	},
 695
 696	// Handles .disabled classes for next/previous buttons
 697	navigation: function() {
 698
 699		// Disable Left/Right when at the start or end of the collection
 700		if ( this.model.cid === this.model.collection.at(0).cid ) {
 701			this.$el.find( '.left' ).addClass( 'disabled' );
 702		}
 703		if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
 704			this.$el.find( '.right' ).addClass( 'disabled' );
 705		}
 706	},
 707
 708	// Performs the actions to effectively close
 709	// the theme details overlay
 710	closeOverlay: function() {
 711		$( 'body' ).removeClass( 'modal-open' );
 712		this.remove();
 713		this.unbind();
 714		this.trigger( 'theme:collapse' );
 715	},
 716
 717	// Confirmation dialog for deleting a theme
 718	deleteTheme: function() {
 719		return confirm( themes.data.settings.confirmDelete );
 720	},
 721
 722	nextTheme: function() {
 723		var self = this;
 724		self.trigger( 'theme:next', self.model.cid );
 725		return false;
 726	},
 727
 728	previousTheme: function() {
 729		var self = this;
 730		self.trigger( 'theme:previous', self.model.cid );
 731		return false;
 732	},
 733
 734	// Checks if the theme screenshot is the old 300px width version
 735	// and adds a corresponding class if it's true
 736	screenshotCheck: function( el ) {
 737		var screenshot, image;
 738
 739		screenshot = el.find( '.screenshot img' );
 740		image = new Image();
 741		image.src = screenshot.attr( 'src' );
 742
 743		// Width check
 744		if ( image.width && image.width <= 300 ) {
 745			el.addClass( 'small-screenshot' );
 746		}
 747	}
 748});
 749
 750// Theme Preview view
 751// Set ups a modal overlay with the expanded theme data
 752themes.view.Preview = themes.view.Details.extend({
 753
 754	className: 'wp-full-overlay expanded',
 755	el: '.theme-install-overlay',
 756
 757	events: {
 758		'click .close-full-overlay': 'close',
 759		'click .collapse-sidebar': 'collapse',
 760		'click .previous-theme': 'previousTheme',
 761		'click .next-theme': 'nextTheme',
 762		'keyup': 'keyEvent'
 763	},
 764
 765	// The HTML template for the theme preview
 766	html: themes.template( 'theme-preview' ),
 767
 768	render: function() {
 769		var self = this,
 770			data = this.model.toJSON();
 771
 772		this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
 773
 774		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: true } );
 775
 776		this.$el.fadeIn( 200, function() {
 777			$( 'body' ).addClass( 'theme-installer-active full-overlay-active' );
 778			$( '.close-full-overlay' ).focus();
 779		});
 780
 781		this.$el.find( 'iframe' ).one( 'load', function() {
 782			self.iframeLoaded();
 783		});
 784	},
 785
 786	iframeLoaded: function() {
 787		this.$el.addClass( 'iframe-ready' );
 788	},
 789
 790	close: function() {
 791		this.$el.fadeOut( 200, function() {
 792			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
 793
 794			// Return focus to the theme div
 795			if ( themes.focusedTheme ) {
 796				themes.focusedTheme.focus();
 797			}
 798		}).removeClass( 'iframe-ready' );
 799
 800		themes.router.navigate( themes.router.baseUrl( '' ) );
 801		this.trigger( 'preview:close' );
 802		this.undelegateEvents();
 803		this.unbind();
 804		return false;
 805	},
 806
 807	collapse: function( event ) {
 808		var $button = $( event.currentTarget );
 809		if ( 'true' === $button.attr( 'aria-expanded' ) ) {
 810			$button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
 811		} else {
 812			$button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
 813		}
 814
 815		this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
 816		return false;
 817	},
 818
 819	keyEvent: function( event ) {
 820		// The escape key closes the preview
 821		if ( event.keyCode === 27 ) {
 822			this.undelegateEvents();
 823			this.close();
 824		}
 825		// The right arrow key, next theme
 826		if ( event.keyCode === 39 ) {
 827			_.once( this.nextTheme() );
 828		}
 829
 830		// The left arrow key, previous theme
 831		if ( event.keyCode === 37 ) {
 832			this.previousTheme();
 833		}
 834	}
 835});
 836
 837// Controls the rendering of div.themes,
 838// a wrapper that will hold all the theme elements
 839themes.view.Themes = wp.Backbone.View.extend({
 840
 841	className: 'themes',
 842	$overlay: $( 'div.theme-overlay' ),
 843
 844	// Number to keep track of scroll position
 845	// while in theme-overlay mode
 846	index: 0,
 847
 848	// The theme count element
 849	count: $( '.wp-core-ui .theme-count' ),
 850
 851	// The live themes count
 852	liveThemeCount: 0,
 853
 854	initialize: function( options ) {
 855		var self = this;
 856
 857		// Set up parent
 858		this.parent = options.parent;
 859
 860		// Set current view to [grid]
 861		this.setView( 'grid' );
 862
 863		// Move the active theme to the beginning of the collection
 864		self.currentTheme();
 865
 866		// When the collection is updated by user input...
 867		this.listenTo( self.collection, 'update', function() {
 868			self.parent.page = 0;
 869			self.currentTheme();
 870			self.render( this );
 871		});
 872
 873		// Update theme count to full result set when available.
 874		this.listenTo( self.collection, 'query:success', function( count ) {
 875			if ( _.isNumber( count ) ) {
 876				self.count.text( count );
 877				self.announceSearchResults( count );
 878			} else {
 879				self.count.text( self.collection.length );
 880				self.announceSearchResults( self.collection.length );
 881			}
 882		});
 883
 884		this.listenTo( self.collection, 'query:empty', function() {
 885			$( 'body' ).addClass( 'no-results' );
 886		});
 887
 888		this.listenTo( this.parent, 'theme:scroll', function() {
 889			self.renderThemes( self.parent.page );
 890		});
 891
 892		this.listenTo( this.parent, 'theme:close', function() {
 893			if ( self.overlay ) {
 894				self.overlay.closeOverlay();
 895			}
 896		} );
 897
 898		// Bind keyboard events.
 899		$( 'body' ).on( 'keyup', function( event ) {
 900			if ( ! self.overlay ) {
 901				return;
 902			}
 903
 904			// Pressing the right arrow key fires a theme:next event
 905			if ( event.keyCode === 39 ) {
 906				self.overlay.nextTheme();
 907			}
 908
 909			// Pressing the left arrow key fires a theme:previous event
 910			if ( event.keyCode === 37 ) {
 911				self.overlay.previousTheme();
 912			}
 913
 914			// Pressing the escape key fires a theme:collapse event
 915			if ( event.keyCode === 27 ) {
 916				self.overlay.collapse( event );
 917			}
 918		});
 919	},
 920
 921	// Manages rendering of theme pages
 922	// and keeping theme count in sync
 923	render: function() {
 924		// Clear the DOM, please
 925		this.$el.empty();
 926
 927		// If the user doesn't have switch capabilities
 928		// or there is only one theme in the collection
 929		// render the detailed view of the active theme
 930		if ( themes.data.themes.length === 1 ) {
 931
 932			// Constructs the view
 933			this.singleTheme = new themes.view.Details({
 934				model: this.collection.models[0]
 935			});
 936
 937			// Render and apply a 'single-theme' class to our container
 938			this.singleTheme.render();
 939			this.$el.addClass( 'single-theme' );
 940			this.$el.append( this.singleTheme.el );
 941		}
 942
 943		// Generate the themes
 944		// Using page instance
 945		// While checking the collection has items
 946		if ( this.options.collection.size() > 0 ) {
 947			this.renderThemes( this.parent.page );
 948		}
 949
 950		// Display a live theme count for the collection
 951		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
 952		this.count.text( this.liveThemeCount );
 953
 954		this.announceSearchResults( this.liveThemeCount );
 955	},
 956
 957	// Iterates through each instance of the collection
 958	// and renders each theme module
 959	renderThemes: function( page ) {
 960		var self = this;
 961
 962		self.instance = self.collection.paginate( page );
 963
 964		// If we have no more themes bail
 965		if ( self.instance.size() === 0 ) {
 966			// Fire a no-more-themes event.
 967			this.parent.trigger( 'theme:end' );
 968			return;
 969		}
 970
 971		// Make sure the add-new stays at the end
 972		if ( ! themes.isInstall && page >= 1 ) {
 973			$( '.add-new-theme' ).remove();
 974		}
 975
 976		// Loop through the themes and setup each theme view
 977		self.instance.each( function( theme ) {
 978			self.theme = new themes.view.Theme({
 979				model: theme,
 980				parent: self
 981			});
 982
 983			// Render the views...
 984			self.theme.render();
 985			// and append them to div.themes
 986			self.$el.append( self.theme.el );
 987
 988			// Binds to theme:expand to show the modal box
 989			// with the theme details
 990			self.listenTo( self.theme, 'theme:expand', self.expand, self );
 991		});
 992
 993		// 'Add new theme' element shown at the end of the grid
 994		if ( ! themes.isInstall && themes.data.settings.canInstall ) {
 995			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>' );
 996		}
 997
 998		this.parent.page++;
 999	},
1000
1001	// Grabs current theme and puts it at the beginning of the collection
1002	currentTheme: function() {
1003		var self = this,
1004			current;
1005
1006		current = self.collection.findWhere({ active: true });
1007
1008		// Move the active theme to the beginning of the collection
1009		if ( current ) {
1010			self.collection.remove( current );
1011			self.collection.add( current, { at:0 } );
1012		}
1013	},
1014
1015	// Sets current view
1016	setView: function( view ) {
1017		return view;
1018	},
1019
1020	// Renders the overlay with the ThemeDetails view
1021	// Uses the current model data
1022	expand: function( id ) {
1023		var self = this;
1024
1025		// Set the current theme model
1026		this.model = self.collection.get( id );
1027
1028		// Trigger a route update for the current model
1029		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
1030
1031		// Sets this.view to 'detail'
1032		this.setView( 'detail' );
1033		$( 'body' ).addClass( 'modal-open' );
1034
1035		// Set up the theme details view
1036		this.overlay = new themes.view.Details({
1037			model: self.model
1038		});
1039
1040		this.overlay.render();
1041		this.$overlay.html( this.overlay.el );
1042
1043		// Bind to theme:next and theme:previous
1044		// triggered by the arrow keys
1045		//
1046		// Keep track of the current model so we
1047		// can infer an index position
1048		this.listenTo( this.overlay, 'theme:next', function() {
1049			// Renders the next theme on the overlay
1050			self.next( [ self.model.cid ] );
1051
1052		})
1053		.listenTo( this.overlay, 'theme:previous', function() {
1054			// Renders the previous theme on the overlay
1055			self.previous( [ self.model.cid ] );
1056		});
1057	},
1058
1059	// This method renders the next theme on the overlay modal
1060	// based on the current position in the collection
1061	// @params [model cid]
1062	next: function( args ) {
1063		var self = this,
1064			model, nextModel;
1065
1066		// Get the current theme
1067		model = self.collection.get( args[0] );
1068		// Find the next model within the collection
1069		nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
1070
1071		// Sanity check which also serves as a boundary test
1072		if ( nextModel !== undefined ) {
1073
1074			// We have a new theme...
1075			// Close the overlay
1076			this.overlay.closeOverlay();
1077
1078			// Trigger a route update for the current model
1079			self.theme.trigger( 'theme:expand', nextModel.cid );
1080
1081		}
1082	},
1083
1084	// This method renders the previous theme on the overlay modal
1085	// based on the current position in the collection
1086	// @params [model cid]
1087	previous: function( args ) {
1088		var self = this,
1089			model, previousModel;
1090
1091		// Get the current theme
1092		model = self.collection.get( args[0] );
1093		// Find the previous model within the collection
1094		previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
1095
1096		if ( previousModel !== undefined ) {
1097
1098			// We have a new theme...
1099			// Close the overlay
1100			this.overlay.closeOverlay();
1101
1102			// Trigger a route update for the current model
1103			self.theme.trigger( 'theme:expand', previousModel.cid );
1104
1105		}
1106	},
1107
1108	// Dispatch audible search results feedback message
1109	announceSearchResults: function( count ) {
1110		if ( 0 === count ) {
1111			wp.a11y.speak( l10n.noThemesFound );
1112		} else {
1113			wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
1114		}
1115	}
1116});
1117
1118// Search input view controller.
1119themes.view.Search = wp.Backbone.View.extend({
1120
1121	tagName: 'input',
1122	className: 'wp-filter-search',
1123	id: 'wp-filter-search-input',
1124	searching: false,
1125
1126	attributes: {
1127		placeholder: l10n.searchPlaceholder,
1128		type: 'search',
1129		'aria-describedby': 'live-search-desc'
1130	},
1131
1132	events: {
1133		'input': 'search',
1134		'keyup': 'search',
1135		'blur': 'pushState'
1136	},
1137
1138	initialize: function( options ) {
1139
1140		this.parent = options.parent;
1141
1142		this.listenTo( this.parent, 'theme:close', function() {
1143			this.searching = false;
1144		} );
1145
1146	},
1147
1148	search: function( event ) {
1149		// Clear on escape.
1150		if ( event.type === 'keyup' && event.which === 27 ) {
1151			event.target.value = '';
1152		}
1153
1154		/**
1155		 * Since doSearch is debounced, it will only run when user input comes to a rest
1156		 */
1157		this.doSearch( event );
1158	},
1159
1160	// Runs a search on the theme collection.
1161	doSearch: _.debounce( function( event ) {
1162		var options = {};
1163
1164		this.collection.doSearch( event.target.value );
1165
1166		// if search is initiated and key is not return
1167		if ( this.searching && event.which !== 13 ) {
1168			options.replace = true;
1169		} else {
1170			this.searching = true;
1171		}
1172
1173		// Update the URL hash
1174		if ( event.target.value ) {
1175			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
1176		} else {
1177			themes.router.navigate( themes.router.baseUrl( '' ) );
1178		}
1179	}, 500 ),
1180
1181	pushState: function( event ) {
1182		var url = themes.router.baseUrl( '' );
1183
1184		if ( event.target.value ) {
1185			url = themes.router.baseUrl( themes.router.searchPath + event.target.value );
1186		}
1187
1188		this.searching = false;
1189		themes.router.navigate( url );
1190
1191	}
1192});
1193
1194// Sets up the routes events for relevant url queries
1195// Listens to [theme] and [search] params
1196themes.Router = Backbone.Router.extend({
1197
1198	routes: {
1199		'themes.php?theme=:slug': 'theme',
1200		'themes.php?search=:query': 'search',
1201		'themes.php?s=:query': 'search',
1202		'themes.php': 'themes',
1203		'': 'themes'
1204	},
1205
1206	baseUrl: function( url ) {
1207		return 'themes.php' + url;
1208	},
1209
1210	themePath: '?theme=',
1211	searchPath: '?search=',
1212
1213	search: function( query ) {
1214		$( '.wp-filter-search' ).val( query );
1215	},
1216
1217	themes: function() {
1218		$( '.wp-filter-search' ).val( '' );
1219	},
1220
1221	navigate: function() {
1222		if ( Backbone.history._hasPushState ) {
1223			Backbone.Router.prototype.navigate.apply( this, arguments );
1224		}
1225	}
1226
1227});
1228
1229// Execute and setup the application
1230themes.Run = {
1231	init: function() {
1232		// Initializes the blog's theme library view
1233		// Create a new collection with data
1234		this.themes = new themes.Collection( themes.data.themes );
1235
1236		// Set up the view
1237		this.view = new themes.view.Appearance({
1238			collection: this.themes
1239		});
1240
1241		this.render();
1242	},
1243
1244	render: function() {
1245
1246		// Render results
1247		this.view.render();
1248		this.routes();
1249
1250		Backbone.history.start({
1251			root: themes.data.settings.adminUrl,
1252			pushState: true,
1253			hashChange: false
1254		});
1255	},
1256
1257	routes: function() {
1258		var self = this;
1259		// Bind to our global thx object
1260		// so that the object is available to sub-views
1261		themes.router = new themes.Router();
1262
1263		// Handles theme details route event
1264		themes.router.on( 'route:theme', function( slug ) {
1265			self.view.view.expand( slug );
1266		});
1267
1268		themes.router.on( 'route:themes', function() {
1269			self.themes.doSearch( '' );
1270			self.view.trigger( 'theme:close' );
1271		});
1272
1273		// Handles search route event
1274		themes.router.on( 'route:search', function() {
1275			$( '.wp-filter-search' ).trigger( 'keyup' );
1276		});
1277
1278		this.extraRoutes();
1279	},
1280
1281	extraRoutes: function() {
1282		return false;
1283	}
1284};
1285
1286// Extend the main Search view
1287themes.view.InstallerSearch =  themes.view.Search.extend({
1288
1289	events: {
1290		'input': 'search',
1291		'keyup': 'search'
1292	},
1293
1294	// Handles Ajax request for searching through themes in public repo
1295	search: function( event ) {
1296
1297		// Tabbing or reverse tabbing into the search input shouldn't trigger a search
1298		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
1299			return;
1300		}
1301
1302		this.collection = this.options.parent.view.collection;
1303
1304		// Clear on escape.
1305		if ( event.type === 'keyup' && event.which === 27 ) {
1306			event.target.value = '';
1307		}
1308
1309		this.doSearch( event.target.value );
1310	},
1311
1312	doSearch: _.debounce( function( value ) {
1313		var request = {};
1314
1315		request.search = value;
1316
1317		// Intercept an [author] search.
1318		//
1319		// If input value starts with `author:` send a request
1320		// for `author` instead of a regular `search`
1321		if ( value.substring( 0, 7 ) === 'author:' ) {
1322			request.search = '';
1323			request.author = value.slice( 7 );
1324		}
1325
1326		// Intercept a [tag] search.
1327		//
1328		// If input value starts with `tag:` send a request
1329		// for `tag` instead of a regular `search`
1330		if ( value.substring( 0, 4 ) === 'tag:' ) {
1331			request.search = '';
1332			request.tag = [ value.slice( 4 ) ];
1333		}
1334
1335		$( '.filter-links li > a.current' ).removeClass( 'current' );
1336		$( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
1337
1338		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1339		// or searching the local cache
1340		this.collection.query( request );
1341
1342		// Set route
1343		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + value ), { replace: true } );
1344	}, 500 )
1345});
1346
1347themes.view.Installer = themes.view.Appearance.extend({
1348
1349	el: '#wpbody-content .wrap',
1350
1351	// Register events for sorting and filters in theme-navigation
1352	events: {
1353		'click .filter-links li > a': 'onSort',
1354		'click .theme-filter': 'onFilter',
1355		'click .drawer-toggle': 'moreFilters',
1356		'click .filter-drawer .apply-filters': 'applyFilters',
1357		'click .filter-group [type="checkbox"]': 'addFilter',
1358		'click .filter-drawer .clear-filters': 'clearFilters',
1359		'click .filtered-by': 'backToFilters',
1360		'click .favorites-form-submit' : 'saveUsername',
1361		'keyup #wporg-username-input': 'saveUsername'
1362	},
1363
1364	// Initial render method
1365	render: function() {
1366		var self = this;
1367
1368		this.search();
1369		this.uploader();
1370
1371		this.collection = new themes.Collection();
1372
1373		// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
1374		this.listenTo( this, 'theme:end', function() {
1375
1376			// Make sure we are not already loading
1377			if ( self.collection.loadingThemes ) {
1378				return;
1379			}
1380
1381			// Set loadingThemes to true and bump page instance of currentQuery.
1382			self.collection.loadingThemes = true;
1383			self.collection.currentQuery.page++;
1384
1385			// Use currentQuery.page to build the themes request.
1386			_.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
1387			self.collection.query( self.collection.currentQuery.request );
1388		});
1389
1390		this.listenTo( this.collection, 'query:success', function() {
1391			$( 'body' ).removeClass( 'loading-content' );
1392			$( '.theme-browser' ).find( 'div.error' ).remove();
1393		});
1394
1395		this.listenTo( this.collection, 'query:fail', function() {
1396			$( 'body' ).removeClass( 'loading-content' );
1397			$( '.theme-browser' ).find( 'div.error' ).remove();
1398			$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p></div>' );
1399		});
1400
1401		if ( this.view ) {
1402			this.view.remove();
1403		}
1404
1405		// Set ups the view and passes the section argument
1406		this.view = new themes.view.Themes({
1407			collection: this.collection,
1408			parent: this
1409		});
1410
1411		// Reset pagination every time the install view handler is run
1412		this.page = 0;
1413
1414		// Render and append
1415		this.$el.find( '.themes' ).remove();
1416		this.view.render();
1417		this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
1418	},
1419
1420	// Handles all the rendering of the public theme directory
1421	browse: function( section ) {
1422		// Create a new collection with the proper theme data
1423		// for each section
1424		this.collection.query( { browse: section } );
1425	},
1426
1427	// Sorting navigation
1428	onSort: function( event ) {
1429		var $el = $( event.target ),
1430			sort = $el.data( 'sort' );
1431
1432		event.preventDefault();
1433
1434		$( 'body' ).removeClass( 'filters-applied show-filters' );
1435
1436		// Bail if this is already active
1437		if ( $el.hasClass( this.activeClass ) ) {
1438			return;
1439		}
1440
1441		this.sort( sort );
1442
1443		// Trigger a router.naviagte update
1444		themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
1445	},
1446
1447	sort: function( sort ) {
1448		this.clearSearch();
1449
1450		$( '.filter-links li > a, .theme-filter' ).removeClass( this.activeClass );
1451		$( '[data-sort="' + sort + '"]' ).addClass( this.activeClass );
1452
1453		if ( 'favorites' === sort ) {
1454			$ ( 'body' ).addClass( 'show-favorites-form' );
1455		} else {
1456			$ ( 'body' ).removeClass( 'show-favorites-form' );
1457		}
1458
1459		this.browse( sort );
1460	},
1461
1462	// Filters and Tags
1463	onFilter: function( event ) {
1464		var request,
1465			$el = $( event.target ),
1466			filter = $el.data( 'filter' );
1467
1468		// Bail if this is already active
1469		if ( $el.hasClass( this.activeClass ) ) {
1470			return;
1471		}
1472
1473		$( '.filter-links li > a, .theme-section' ).removeClass( this.activeClass );
1474		$el.addClass( this.activeClass );
1475
1476		if ( ! filter ) {
1477			return;
1478		}
1479
1480		// Construct the filter request
1481		// using the default values
1482		filter = _.union( filter, this.filtersChecked() );
1483		request = { tag: [ filter ] };
1484
1485		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1486		// or searching the local cache
1487		this.collection.query( request );
1488	},
1489
1490	// Clicking on a checkbox to add another filter to the request
1491	addFilter: function() {
1492		this.filtersChecked();
1493	},
1494
1495	// Applying filters triggers a tag request
1496	applyFilters: function( event ) {
1497		var name,
1498			tags = this.filtersChecked(),
1499			request = { tag: tags },
1500			filteringBy = $( '.filtered-by .tags' );
1501
1502		if ( event ) {
1503			event.preventDefault();
1504		}
1505
1506		$( 'body' ).addClass( 'filters-applied' );
1507		$( '.filter-links li > a.current' ).removeClass( 'current' );
1508		filteringBy.empty();
1509
1510		_.each( tags, function( tag ) {
1511			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
1512			filteringBy.append( '<span class="tag">' + name + '</span>' );
1513		});
1514
1515		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1516		// or searching the local cache
1517		this.collection.query( request );
1518	},
1519
1520	// Save the user's WordPress.org username and get his favorite themes.
1521	saveUsername: function ( event ) {
1522		var username = $( '#wporg-username-input' ).val(),
1523			request = { browse: 'favorites', user: username },
1524			that = this;
1525
1526		if ( event ) {
1527			event.preventDefault();
1528		}
1529
1530		// save username on enter
1531		if ( event.type === 'keyup' && event.which !== 13 ) {
1532			return;
1533		}
1534
1535		return wp.ajax.send( 'save-wporg-username', {
1536			data: {
1537				username: username
1538			},
1539			success: function () {
1540				// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1541				// or searching the local cache
1542				that.collection.query( request );
1543			}
1544		} );
1545	},
1546
1547	// Get the checked filters
1548	// @return {array} of tags or false
1549	filtersChecked: function() {
1550		var items = $( '.filter-group' ).find( ':checkbox' ),
1551			tags = [];
1552
1553		_.each( items.filter( ':checked' ), function( item ) {
1554			tags.push( $( item ).prop( 'value' ) );
1555		});
1556
1557		// When no filters are checked, restore initial state and return
1558		if ( tags.length === 0 ) {
1559			$( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
1560			$( '.filter-drawer .clear-filters' ).hide();
1561			$( 'body' ).removeClass( 'filters-applied' );
1562			return false;
1563		}
1564
1565		$( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
1566		$( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
1567
1568		return tags;
1569	},
1570
1571	activeClass: 'current',
1572
1573	// Overwrite search container class to append search
1574	// in new location
1575	searchContainer: $( '.wp-filter .search-form' ),
1576
1577	uploader: function() {
1578		$( 'a.upload' ).on( 'click', function( event ) {
1579			event.preventDefault();
1580			$( 'body' ).addClass( 'show-upload-theme' );
1581			themes.router.navigate( themes.router.baseUrl( '?upload' ), { replace: true } );
1582		});
1583		$( 'a.browse-themes' ).on( 'click', function( event ) {
1584			event.preventDefault();
1585			$( 'body' ).removeClass( 'show-upload-theme' );
1586			themes.router.navigate( themes.router.baseUrl( '' ), { replace: true } );
1587		});
1588	},
1589
1590	// Toggle the full filters navigation
1591	moreFilters: function( event ) {
1592		event.preventDefault();
1593
1594		if ( $( 'body' ).hasClass( 'filters-applied' ) ) {
1595			return this.backToFilters();
1596		}
1597
1598		// If the filters section is opened and filters are checked
1599		// run the relevant query collapsing to filtered-by state
1600		if ( $( 'body' ).hasClass( 'show-filters' ) && this.filtersChecked() ) {
1601			return this.addFilter();
1602		}
1603
1604		this.clearSearch();
1605
1606		themes.router.navigate( themes.router.baseUrl( '' ) );
1607		$( 'body' ).toggleClass( 'show-filters' );
1608	},
1609
1610	// Clears all the checked filters
1611	// @uses filtersChecked()
1612	clearFilters: function( event ) {
1613		var items = $( '.filter-group' ).find( ':checkbox' ),
1614			self = this;
1615
1616		event.preventDefault();
1617
1618		_.each( items.filter( ':checked' ), function( item ) {
1619			$( item ).prop( 'checked', false );
1620			return self.filtersChecked();
1621		});
1622	},
1623
1624	backToFilters: function( event ) {
1625		if ( event ) {
1626			event.preventDefault();
1627		}
1628
1629		$( 'body' ).removeClass( 'filters-applied' );
1630	},
1631
1632	clearSearch: function() {
1633		$( '#wp-filter-search-input').val( '' );
1634	}
1635});
1636
1637themes.InstallerRouter = Backbone.Router.extend({
1638	routes: {
1639		'theme-install.php?theme=:slug': 'preview',
1640		'theme-install.php?browse=:sort': 'sort',
1641		'theme-install.php?upload': 'upload',
1642		'theme-install.php?search=:query': 'search',
1643		'theme-install.php': 'sort'
1644	},
1645
1646	baseUrl: function( url ) {
1647		return 'theme-install.php' + url;
1648	},
1649
1650	themePath: '?theme=',
1651	browsePath: '?browse=',
1652	searchPath: '?search=',
1653
1654	search: function( query ) {
1655		$( '.wp-filter-search' ).val( query );
1656	},
1657
1658	navigate: function() {
1659		if ( Backbone.history._hasPushState ) {
1660			Backbone.Router.prototype.navigate.apply( this, arguments );
1661		}
1662	}
1663});
1664
1665
1666themes.RunInstaller = {
1667
1668	init: function() {
1669		// Set up the view
1670		// Passes the default 'section' as an option
1671		this.view = new themes.view.Installer({
1672			section: 'featured',
1673			SearchView: themes.view.InstallerSearch
1674		});
1675
1676		// Render results
1677		this.render();
1678
1679	},
1680
1681	render: function() {
1682
1683		// Render results
1684		this.view.render();
1685		this.routes();
1686
1687		Backbone.history.start({
1688			root: themes.data.settings.adminUrl,
1689			pushState: true,
1690			hashChange: false
1691		});
1692	},
1693
1694	routes: function() {
1695		var self = this,
1696			request = {};
1697
1698		// Bind to our global `wp.themes` object
1699		// so that the router is available to sub-views
1700		themes.router = new themes.InstallerRouter();
1701
1702		// Handles `theme` route event
1703		// Queries the API for the passed theme slug
1704		themes.router.on( 'route:preview', function( slug ) {
1705			request.theme = slug;
1706			self.view.collection.query( request );
1707			self.view.collection.once( 'update', function() {
1708				self.view.view.theme.preview();
1709			});
1710		});
1711
1712		// Handles sorting / browsing routes
1713		// Also handles the root URL triggering a sort request
1714		// for `featured`, the default view
1715		themes.router.on( 'route:sort', function( sort ) {
1716			if ( ! sort ) {
1717				sort = 'featured';
1718			}
1719			self.view.sort( sort );
1720			self.view.trigger( 'theme:close' );
1721		});
1722
1723		// Support the `upload` route by going straight to upload section
1724		themes.router.on( 'route:upload', function() {
1725			$( 'a.upload' ).trigger( 'click' );
1726		});
1727
1728		// The `search` route event. The router populates the input field.
1729		themes.router.on( 'route:search', function() {
1730			$( '.wp-filter-search' ).focus().trigger( 'keyup' );
1731		});
1732
1733		this.extraRoutes();
1734	},
1735
1736	extraRoutes: function() {
1737		return false;
1738	}
1739};
1740
1741// Ready...
1742$( document ).ready(function() {
1743	if ( themes.isInstall ) {
1744		themes.RunInstaller.init();
1745	} else {
1746		themes.Run.init();
1747	}
1748
1749	$( '.broken-themes .delete-theme' ).on( 'click', function() {
1750		return confirm( _wpThemeSettings.settings.confirmDelete );
1751	});
1752});
1753
1754})( jQuery );
1755
1756// Align theme browser thickbox
1757var tb_position;
1758jQuery(document).ready( function($) {
1759	tb_position = function() {
1760		var tbWindow = $('#TB_window'),
1761			width = $(window).width(),
1762			H = $(window).height(),
1763			W = ( 1040 < width ) ? 1040 : width,
1764			adminbar_height = 0;
1765
1766		if ( $('#wpadminbar').length ) {
1767			adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
1768		}
1769
1770		if ( tbWindow.size() ) {
1771			tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
1772			$('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
1773			tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
1774			if ( typeof document.body.style.maxWidth !== 'undefined' ) {
1775				tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
1776			}
1777		}
1778	};
1779
1780	$(window).resize(function(){ tb_position(); });
1781});