PageRenderTime 8ms CodeModel.GetById 2ms app.highlight 57ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-admin/js/theme.js

https://gitlab.com/tulipsnepal/asa
JavaScript | 1726 lines | 1034 code | 361 blank | 331 comment | 125 complexity | ce08e4628996a70a5d5deac9221e1130 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 h2: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		// Bail if the user scrolled on a touch device
 458		if ( this.touchDrag === true ) {
 459			return this.touchDrag = false;
 460		}
 461
 462		// Allow direct link path to installing a theme.
 463		if ( $( event.target ).hasClass( 'button-primary' ) ) {
 464			return;
 465		}
 466
 467		// 'enter' and 'space' keys expand the details view when a theme is :focused
 468		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
 469			return;
 470		}
 471
 472		// pressing enter while focused on the buttons shouldn't open the preview
 473		if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
 474			return;
 475		}
 476
 477		event.preventDefault();
 478
 479		event = event || window.event;
 480
 481		// Set focus to current theme.
 482		themes.focusedTheme = this.$el;
 483
 484		// Construct a new Preview view.
 485		preview = new themes.view.Preview({
 486			model: this.model
 487		});
 488
 489		// Render the view and append it.
 490		preview.render();
 491		this.setNavButtonsState();
 492
 493		// Hide previous/next navigation if there is only one theme
 494		if ( this.model.collection.length === 1 ) {
 495			preview.$el.addClass( 'no-navigation' );
 496		} else {
 497			preview.$el.removeClass( 'no-navigation' );
 498		}
 499
 500		// Append preview
 501		$( 'div.wrap' ).append( preview.el );
 502
 503		// Listen to our preview object
 504		// for `theme:next` and `theme:previous` events.
 505		this.listenTo( preview, 'theme:next', function() {
 506
 507			// Keep local track of current theme model.
 508			current = self.model;
 509
 510			// If we have ventured away from current model update the current model position.
 511			if ( ! _.isUndefined( self.current ) ) {
 512				current = self.current;
 513			}
 514
 515			// Get next theme model.
 516			self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
 517
 518			// If we have no more themes, bail.
 519			if ( _.isUndefined( self.current ) ) {
 520				self.options.parent.parent.trigger( 'theme:end' );
 521				return self.current = current;
 522			}
 523
 524			preview.model = self.current;
 525
 526			// Render and append.
 527			preview.render();
 528			this.setNavButtonsState();
 529			$( '.next-theme' ).focus();
 530		})
 531		.listenTo( preview, 'theme:previous', function() {
 532
 533			// Keep track of current theme model.
 534			current = self.model;
 535
 536			// Bail early if we are at the beginning of the collection
 537			if ( self.model.collection.indexOf( self.current ) === 0 ) {
 538				return;
 539			}
 540
 541			// If we have ventured away from current model update the current model position.
 542			if ( ! _.isUndefined( self.current ) ) {
 543				current = self.current;
 544			}
 545
 546			// Get previous theme model.
 547			self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
 548
 549			// If we have no more themes, bail.
 550			if ( _.isUndefined( self.current ) ) {
 551				return;
 552			}
 553
 554			preview.model = self.current;
 555
 556			// Render and append.
 557			preview.render();
 558			this.setNavButtonsState();
 559			$( '.previous-theme' ).focus();
 560		});
 561
 562		this.listenTo( preview, 'preview:close', function() {
 563			self.current = self.model;
 564		});
 565	},
 566
 567	// Handles .disabled classes for previous/next buttons in theme installer preview
 568	setNavButtonsState: function() {
 569		var $themeInstaller = $( '.theme-install-overlay' ),
 570			current = _.isUndefined( this.current ) ? this.model : this.current;
 571
 572		// Disable previous at the zero position
 573		if ( 0 === this.model.collection.indexOf( current ) ) {
 574			$themeInstaller.find( '.previous-theme' ).addClass( 'disabled' );
 575		}
 576
 577		// Disable next if the next model is undefined
 578		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
 579			$themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
 580		}
 581	}
 582});
 583
 584// Theme Details view
 585// Set ups a modal overlay with the expanded theme data
 586themes.view.Details = wp.Backbone.View.extend({
 587
 588	// Wrap theme data on a div.theme element
 589	className: 'theme-overlay',
 590
 591	events: {
 592		'click': 'collapse',
 593		'click .delete-theme': 'deleteTheme',
 594		'click .left': 'previousTheme',
 595		'click .right': 'nextTheme'
 596	},
 597
 598	// The HTML template for the theme overlay
 599	html: themes.template( 'theme-single' ),
 600
 601	render: function() {
 602		var data = this.model.toJSON();
 603		this.$el.html( this.html( data ) );
 604		// Renders active theme styles
 605		this.activeTheme();
 606		// Set up navigation events
 607		this.navigation();
 608		// Checks screenshot size
 609		this.screenshotCheck( this.$el );
 610		// Contain "tabbing" inside the overlay
 611		this.containFocus( this.$el );
 612	},
 613
 614	// Adds a class to the currently active theme
 615	// and to the overlay in detailed view mode
 616	activeTheme: function() {
 617		// Check the model has the active property
 618		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
 619	},
 620
 621	// Keeps :focus within the theme details elements
 622	containFocus: function( $el ) {
 623		var $target;
 624
 625		// Move focus to the primary action
 626		_.delay( function() {
 627			$( '.theme-wrap a.button-primary:visible' ).focus();
 628		}, 500 );
 629
 630		$el.on( 'keydown.wp-themes', function( event ) {
 631
 632			// Tab key
 633			if ( event.which === 9 ) {
 634				$target = $( event.target );
 635
 636				// Keep focus within the overlay by making the last link on theme actions
 637				// switch focus to button.left on tabbing and vice versa
 638				if ( $target.is( 'button.left' ) && event.shiftKey ) {
 639					$el.find( '.theme-actions a:last-child' ).focus();
 640					event.preventDefault();
 641				} else if ( $target.is( '.theme-actions a:last-child' ) ) {
 642					$el.find( 'button.left' ).focus();
 643					event.preventDefault();
 644				}
 645			}
 646		});
 647	},
 648
 649	// Single theme overlay screen
 650	// It's shown when clicking a theme
 651	collapse: function( event ) {
 652		var self = this,
 653			scroll;
 654
 655		event = event || window.event;
 656
 657		// Prevent collapsing detailed view when there is only one theme available
 658		if ( themes.data.themes.length === 1 ) {
 659			return;
 660		}
 661
 662		// Detect if the click is inside the overlay
 663		// and don't close it unless the target was
 664		// the div.back button
 665		if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
 666
 667			// Add a temporary closing class while overlay fades out
 668			$( 'body' ).addClass( 'closing-overlay' );
 669
 670			// With a quick fade out animation
 671			this.$el.fadeOut( 130, function() {
 672				// Clicking outside the modal box closes the overlay
 673				$( 'body' ).removeClass( 'closing-overlay' );
 674				// Handle event cleanup
 675				self.closeOverlay();
 676
 677				// Get scroll position to avoid jumping to the top
 678				scroll = document.body.scrollTop;
 679
 680				// Clean the url structure
 681				themes.router.navigate( themes.router.baseUrl( '' ) );
 682
 683				// Restore scroll position
 684				document.body.scrollTop = scroll;
 685
 686				// Return focus to the theme div
 687				if ( themes.focusedTheme ) {
 688					themes.focusedTheme.focus();
 689				}
 690			});
 691		}
 692	},
 693
 694	// Handles .disabled classes for next/previous buttons
 695	navigation: function() {
 696
 697		// Disable Left/Right when at the start or end of the collection
 698		if ( this.model.cid === this.model.collection.at(0).cid ) {
 699			this.$el.find( '.left' ).addClass( 'disabled' );
 700		}
 701		if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
 702			this.$el.find( '.right' ).addClass( 'disabled' );
 703		}
 704	},
 705
 706	// Performs the actions to effectively close
 707	// the theme details overlay
 708	closeOverlay: function() {
 709		$( 'body' ).removeClass( 'modal-open' );
 710		this.remove();
 711		this.unbind();
 712		this.trigger( 'theme:collapse' );
 713	},
 714
 715	// Confirmation dialog for deleting a theme
 716	deleteTheme: function() {
 717		return confirm( themes.data.settings.confirmDelete );
 718	},
 719
 720	nextTheme: function() {
 721		var self = this;
 722		self.trigger( 'theme:next', self.model.cid );
 723		return false;
 724	},
 725
 726	previousTheme: function() {
 727		var self = this;
 728		self.trigger( 'theme:previous', self.model.cid );
 729		return false;
 730	},
 731
 732	// Checks if the theme screenshot is the old 300px width version
 733	// and adds a corresponding class if it's true
 734	screenshotCheck: function( el ) {
 735		var screenshot, image;
 736
 737		screenshot = el.find( '.screenshot img' );
 738		image = new Image();
 739		image.src = screenshot.attr( 'src' );
 740
 741		// Width check
 742		if ( image.width && image.width <= 300 ) {
 743			el.addClass( 'small-screenshot' );
 744		}
 745	}
 746});
 747
 748// Theme Preview view
 749// Set ups a modal overlay with the expanded theme data
 750themes.view.Preview = themes.view.Details.extend({
 751
 752	className: 'wp-full-overlay expanded',
 753	el: '.theme-install-overlay',
 754
 755	events: {
 756		'click .close-full-overlay': 'close',
 757		'click .collapse-sidebar': 'collapse',
 758		'click .previous-theme': 'previousTheme',
 759		'click .next-theme': 'nextTheme',
 760		'keyup': 'keyEvent'
 761	},
 762
 763	// The HTML template for the theme preview
 764	html: themes.template( 'theme-preview' ),
 765
 766	render: function() {
 767		var data = this.model.toJSON();
 768
 769		this.$el.html( this.html( data ) );
 770
 771		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: true } );
 772
 773		this.$el.fadeIn( 200, function() {
 774			$( 'body' ).addClass( 'theme-installer-active full-overlay-active' );
 775			$( '.close-full-overlay' ).focus();
 776		});
 777	},
 778
 779	close: function() {
 780		this.$el.fadeOut( 200, function() {
 781			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
 782
 783			// Return focus to the theme div
 784			if ( themes.focusedTheme ) {
 785				themes.focusedTheme.focus();
 786			}
 787		});
 788
 789		themes.router.navigate( themes.router.baseUrl( '' ) );
 790		this.trigger( 'preview:close' );
 791		this.undelegateEvents();
 792		this.unbind();
 793		return false;
 794	},
 795
 796	collapse: function() {
 797
 798		this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
 799		return false;
 800	},
 801
 802	keyEvent: function( event ) {
 803		// The escape key closes the preview
 804		if ( event.keyCode === 27 ) {
 805			this.undelegateEvents();
 806			this.close();
 807		}
 808		// The right arrow key, next theme
 809		if ( event.keyCode === 39 ) {
 810			_.once( this.nextTheme() );
 811		}
 812
 813		// The left arrow key, previous theme
 814		if ( event.keyCode === 37 ) {
 815			this.previousTheme();
 816		}
 817	}
 818});
 819
 820// Controls the rendering of div.themes,
 821// a wrapper that will hold all the theme elements
 822themes.view.Themes = wp.Backbone.View.extend({
 823
 824	className: 'themes',
 825	$overlay: $( 'div.theme-overlay' ),
 826
 827	// Number to keep track of scroll position
 828	// while in theme-overlay mode
 829	index: 0,
 830
 831	// The theme count element
 832	count: $( '.wp-core-ui .theme-count' ),
 833
 834	// The live themes count
 835	liveThemeCount: 0,
 836
 837	initialize: function( options ) {
 838		var self = this;
 839
 840		// Set up parent
 841		this.parent = options.parent;
 842
 843		// Set current view to [grid]
 844		this.setView( 'grid' );
 845
 846		// Move the active theme to the beginning of the collection
 847		self.currentTheme();
 848
 849		// When the collection is updated by user input...
 850		this.listenTo( self.collection, 'update', function() {
 851			self.parent.page = 0;
 852			self.currentTheme();
 853			self.render( this );
 854		});
 855
 856		// Update theme count to full result set when available.
 857		this.listenTo( self.collection, 'query:success', function( count ) {
 858			if ( _.isNumber( count ) ) {
 859				self.count.text( count );
 860				self.announceSearchResults( count );
 861			} else {
 862				self.count.text( self.collection.length );
 863				self.announceSearchResults( self.collection.length );
 864			}
 865		});
 866
 867		this.listenTo( self.collection, 'query:empty', function() {
 868			$( 'body' ).addClass( 'no-results' );
 869		});
 870
 871		this.listenTo( this.parent, 'theme:scroll', function() {
 872			self.renderThemes( self.parent.page );
 873		});
 874
 875		this.listenTo( this.parent, 'theme:close', function() {
 876			if ( self.overlay ) {
 877				self.overlay.closeOverlay();
 878			}
 879		} );
 880
 881		// Bind keyboard events.
 882		$( 'body' ).on( 'keyup', function( event ) {
 883			if ( ! self.overlay ) {
 884				return;
 885			}
 886
 887			// Pressing the right arrow key fires a theme:next event
 888			if ( event.keyCode === 39 ) {
 889				self.overlay.nextTheme();
 890			}
 891
 892			// Pressing the left arrow key fires a theme:previous event
 893			if ( event.keyCode === 37 ) {
 894				self.overlay.previousTheme();
 895			}
 896
 897			// Pressing the escape key fires a theme:collapse event
 898			if ( event.keyCode === 27 ) {
 899				self.overlay.collapse( event );
 900			}
 901		});
 902	},
 903
 904	// Manages rendering of theme pages
 905	// and keeping theme count in sync
 906	render: function() {
 907		// Clear the DOM, please
 908		this.$el.empty();
 909
 910		// If the user doesn't have switch capabilities
 911		// or there is only one theme in the collection
 912		// render the detailed view of the active theme
 913		if ( themes.data.themes.length === 1 ) {
 914
 915			// Constructs the view
 916			this.singleTheme = new themes.view.Details({
 917				model: this.collection.models[0]
 918			});
 919
 920			// Render and apply a 'single-theme' class to our container
 921			this.singleTheme.render();
 922			this.$el.addClass( 'single-theme' );
 923			this.$el.append( this.singleTheme.el );
 924		}
 925
 926		// Generate the themes
 927		// Using page instance
 928		// While checking the collection has items
 929		if ( this.options.collection.size() > 0 ) {
 930			this.renderThemes( this.parent.page );
 931		}
 932
 933		// Display a live theme count for the collection
 934		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
 935		this.count.text( this.liveThemeCount );
 936
 937		this.announceSearchResults( this.liveThemeCount );
 938	},
 939
 940	// Iterates through each instance of the collection
 941	// and renders each theme module
 942	renderThemes: function( page ) {
 943		var self = this;
 944
 945		self.instance = self.collection.paginate( page );
 946
 947		// If we have no more themes bail
 948		if ( self.instance.size() === 0 ) {
 949			// Fire a no-more-themes event.
 950			this.parent.trigger( 'theme:end' );
 951			return;
 952		}
 953
 954		// Make sure the add-new stays at the end
 955		if ( page >= 1 ) {
 956			$( '.add-new-theme' ).remove();
 957		}
 958
 959		// Loop through the themes and setup each theme view
 960		self.instance.each( function( theme ) {
 961			self.theme = new themes.view.Theme({
 962				model: theme,
 963				parent: self
 964			});
 965
 966			// Render the views...
 967			self.theme.render();
 968			// and append them to div.themes
 969			self.$el.append( self.theme.el );
 970
 971			// Binds to theme:expand to show the modal box
 972			// with the theme details
 973			self.listenTo( self.theme, 'theme:expand', self.expand, self );
 974		});
 975
 976		// 'Add new theme' element shown at the end of the grid
 977		if ( themes.data.settings.canInstall ) {
 978			this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h3 class="theme-name">' + l10n.addNew + '</h3></a></div>' );
 979		}
 980
 981		this.parent.page++;
 982	},
 983
 984	// Grabs current theme and puts it at the beginning of the collection
 985	currentTheme: function() {
 986		var self = this,
 987			current;
 988
 989		current = self.collection.findWhere({ active: true });
 990
 991		// Move the active theme to the beginning of the collection
 992		if ( current ) {
 993			self.collection.remove( current );
 994			self.collection.add( current, { at:0 } );
 995		}
 996	},
 997
 998	// Sets current view
 999	setView: function( view ) {
1000		return view;
1001	},
1002
1003	// Renders the overlay with the ThemeDetails view
1004	// Uses the current model data
1005	expand: function( id ) {
1006		var self = this;
1007
1008		// Set the current theme model
1009		this.model = self.collection.get( id );
1010
1011		// Trigger a route update for the current model
1012		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
1013
1014		// Sets this.view to 'detail'
1015		this.setView( 'detail' );
1016		$( 'body' ).addClass( 'modal-open' );
1017
1018		// Set up the theme details view
1019		this.overlay = new themes.view.Details({
1020			model: self.model
1021		});
1022
1023		this.overlay.render();
1024		this.$overlay.html( this.overlay.el );
1025
1026		// Bind to theme:next and theme:previous
1027		// triggered by the arrow keys
1028		//
1029		// Keep track of the current model so we
1030		// can infer an index position
1031		this.listenTo( this.overlay, 'theme:next', function() {
1032			// Renders the next theme on the overlay
1033			self.next( [ self.model.cid ] );
1034
1035		})
1036		.listenTo( this.overlay, 'theme:previous', function() {
1037			// Renders the previous theme on the overlay
1038			self.previous( [ self.model.cid ] );
1039		});
1040	},
1041
1042	// This method renders the next theme on the overlay modal
1043	// based on the current position in the collection
1044	// @params [model cid]
1045	next: function( args ) {
1046		var self = this,
1047			model, nextModel;
1048
1049		// Get the current theme
1050		model = self.collection.get( args[0] );
1051		// Find the next model within the collection
1052		nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
1053
1054		// Sanity check which also serves as a boundary test
1055		if ( nextModel !== undefined ) {
1056
1057			// We have a new theme...
1058			// Close the overlay
1059			this.overlay.closeOverlay();
1060
1061			// Trigger a route update for the current model
1062			self.theme.trigger( 'theme:expand', nextModel.cid );
1063
1064		}
1065	},
1066
1067	// This method renders the previous theme on the overlay modal
1068	// based on the current position in the collection
1069	// @params [model cid]
1070	previous: function( args ) {
1071		var self = this,
1072			model, previousModel;
1073
1074		// Get the current theme
1075		model = self.collection.get( args[0] );
1076		// Find the previous model within the collection
1077		previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
1078
1079		if ( previousModel !== undefined ) {
1080
1081			// We have a new theme...
1082			// Close the overlay
1083			this.overlay.closeOverlay();
1084
1085			// Trigger a route update for the current model
1086			self.theme.trigger( 'theme:expand', previousModel.cid );
1087
1088		}
1089	},
1090
1091	// Dispatch audible search results feedback message
1092	announceSearchResults: function( count ) {
1093		if ( 0 === count ) {
1094			wp.a11y.speak( l10n.noThemesFound );
1095		} else {
1096			wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
1097		}
1098	}
1099});
1100
1101// Search input view controller.
1102themes.view.Search = wp.Backbone.View.extend({
1103
1104	tagName: 'input',
1105	className: 'wp-filter-search',
1106	id: 'wp-filter-search-input',
1107	searching: false,
1108
1109	attributes: {
1110		placeholder: l10n.searchPlaceholder,
1111		type: 'search',
1112		'aria-describedby': 'live-search-desc'
1113	},
1114
1115	events: {
1116		'input': 'search',
1117		'keyup': 'search',
1118		'blur': 'pushState'
1119	},
1120
1121	initialize: function( options ) {
1122
1123		this.parent = options.parent;
1124
1125		this.listenTo( this.parent, 'theme:close', function() {
1126			this.searching = false;
1127		} );
1128
1129	},
1130
1131	search: function( event ) {
1132		// Clear on escape.
1133		if ( event.type === 'keyup' && event.which === 27 ) {
1134			event.target.value = '';
1135		}
1136
1137		/**
1138		 * Since doSearch is debounced, it will only run when user input comes to a rest
1139		 */
1140		this.doSearch( event );
1141	},
1142
1143	// Runs a search on the theme collection.
1144	doSearch: _.debounce( function( event ) {
1145		var options = {};
1146
1147		this.collection.doSearch( event.target.value );
1148
1149		// if search is initiated and key is not return
1150		if ( this.searching && event.which !== 13 ) {
1151			options.replace = true;
1152		} else {
1153			this.searching = true;
1154		}
1155
1156		// Update the URL hash
1157		if ( event.target.value ) {
1158			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
1159		} else {
1160			themes.router.navigate( themes.router.baseUrl( '' ) );
1161		}
1162	}, 500 ),
1163
1164	pushState: function( event ) {
1165		var url = themes.router.baseUrl( '' );
1166
1167		if ( event.target.value ) {
1168			url = themes.router.baseUrl( themes.router.searchPath + event.target.value );
1169		}
1170
1171		this.searching = false;
1172		themes.router.navigate( url );
1173
1174	}
1175});
1176
1177// Sets up the routes events for relevant url queries
1178// Listens to [theme] and [search] params
1179themes.Router = Backbone.Router.extend({
1180
1181	routes: {
1182		'themes.php?theme=:slug': 'theme',
1183		'themes.php?search=:query': 'search',
1184		'themes.php?s=:query': 'search',
1185		'themes.php': 'themes',
1186		'': 'themes'
1187	},
1188
1189	baseUrl: function( url ) {
1190		return 'themes.php' + url;
1191	},
1192
1193	themePath: '?theme=',
1194	searchPath: '?search=',
1195
1196	search: function( query ) {
1197		$( '.wp-filter-search' ).val( query );
1198	},
1199
1200	themes: function() {
1201		$( '.wp-filter-search' ).val( '' );
1202	},
1203
1204	navigate: function() {
1205		if ( Backbone.history._hasPushState ) {
1206			Backbone.Router.prototype.navigate.apply( this, arguments );
1207		}
1208	}
1209
1210});
1211
1212// Execute and setup the application
1213themes.Run = {
1214	init: function() {
1215		// Initializes the blog's theme library view
1216		// Create a new collection with data
1217		this.themes = new themes.Collection( themes.data.themes );
1218
1219		// Set up the view
1220		this.view = new themes.view.Appearance({
1221			collection: this.themes
1222		});
1223
1224		this.render();
1225	},
1226
1227	render: function() {
1228
1229		// Render results
1230		this.view.render();
1231		this.routes();
1232
1233		Backbone.history.start({
1234			root: themes.data.settings.adminUrl,
1235			pushState: true,
1236			hashChange: false
1237		});
1238	},
1239
1240	routes: function() {
1241		var self = this;
1242		// Bind to our global thx object
1243		// so that the object is available to sub-views
1244		themes.router = new themes.Router();
1245
1246		// Handles theme details route event
1247		themes.router.on( 'route:theme', function( slug ) {
1248			self.view.view.expand( slug );
1249		});
1250
1251		themes.router.on( 'route:themes', function() {
1252			self.themes.doSearch( '' );
1253			self.view.trigger( 'theme:close' );
1254		});
1255
1256		// Handles search route event
1257		themes.router.on( 'route:search', function() {
1258			$( '.wp-filter-search' ).trigger( 'keyup' );
1259		});
1260
1261		this.extraRoutes();
1262	},
1263
1264	extraRoutes: function() {
1265		return false;
1266	}
1267};
1268
1269// Extend the main Search view
1270themes.view.InstallerSearch =  themes.view.Search.extend({
1271
1272	events: {
1273		'input': 'search',
1274		'keyup': 'search'
1275	},
1276
1277	// Handles Ajax request for searching through themes in public repo
1278	search: function( event ) {
1279
1280		// Tabbing or reverse tabbing into the search input shouldn't trigger a search
1281		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
1282			return;
1283		}
1284
1285		this.collection = this.options.parent.view.collection;
1286
1287		// Clear on escape.
1288		if ( event.type === 'keyup' && event.which === 27 ) {
1289			event.target.value = '';
1290		}
1291
1292		this.doSearch( event.target.value );
1293	},
1294
1295	doSearch: _.debounce( function( value ) {
1296		var request = {};
1297
1298		request.search = value;
1299
1300		// Intercept an [author] search.
1301		//
1302		// If input value starts with `author:` send a request
1303		// for `author` instead of a regular `search`
1304		if ( value.substring( 0, 7 ) === 'author:' ) {
1305			request.search = '';
1306			request.author = value.slice( 7 );
1307		}
1308
1309		// Intercept a [tag] search.
1310		//
1311		// If input value starts with `tag:` send a request
1312		// for `tag` instead of a regular `search`
1313		if ( value.substring( 0, 4 ) === 'tag:' ) {
1314			request.search = '';
1315			request.tag = [ value.slice( 4 ) ];
1316		}
1317
1318		$( '.filter-links li > a.current' ).removeClass( 'current' );
1319		$( 'body' ).removeClass( 'show-filters filters-applied' );
1320
1321		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1322		// or searching the local cache
1323		this.collection.query( request );
1324
1325		// Set route
1326		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + value ), { replace: true } );
1327	}, 500 )
1328});
1329
1330themes.view.Installer = themes.view.Appearance.extend({
1331
1332	el: '#wpbody-content .wrap',
1333
1334	// Register events for sorting and filters in theme-navigation
1335	events: {
1336		'click .filter-links li > a': 'onSort',
1337		'click .theme-filter': 'onFilter',
1338		'click .drawer-toggle': 'moreFilters',
1339		'click .filter-drawer .apply-filters': 'applyFilters',
1340		'click .filter-group [type="checkbox"]': 'addFilter',
1341		'click .filter-drawer .clear-filters': 'clearFilters',
1342		'click .filtered-by': 'backToFilters'
1343	},
1344
1345	// Initial render method
1346	render: function() {
1347		var self = this;
1348
1349		this.search();
1350		this.uploader();
1351
1352		this.collection = new themes.Collection();
1353
1354		// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
1355		this.listenTo( this, 'theme:end', function() {
1356
1357			// Make sure we are not already loading
1358			if ( self.collection.loadingThemes ) {
1359				return;
1360			}
1361
1362			// Set loadingThemes to true and bump page instance of currentQuery.
1363			self.collection.loadingThemes = true;
1364			self.collection.currentQuery.page++;
1365
1366			// Use currentQuery.page to build the themes request.
1367			_.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
1368			self.collection.query( self.collection.currentQuery.request );
1369		});
1370
1371		this.listenTo( this.collection, 'query:success', function() {
1372			$( 'body' ).removeClass( 'loading-content' );
1373			$( '.theme-browser' ).find( 'div.error' ).remove();
1374		});
1375
1376		this.listenTo( this.collection, 'query:fail', function() {
1377			$( 'body' ).removeClass( 'loading-content' );
1378			$( '.theme-browser' ).find( 'div.error' ).remove();
1379			$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p></div>' );
1380		});
1381
1382		if ( this.view ) {
1383			this.view.remove();
1384		}
1385
1386		// Set ups the view and passes the section argument
1387		this.view = new themes.view.Themes({
1388			collection: this.collection,
1389			parent: this
1390		});
1391
1392		// Reset pagination every time the install view handler is run
1393		this.page = 0;
1394
1395		// Render and append
1396		this.$el.find( '.themes' ).remove();
1397		this.view.render();
1398		this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
1399	},
1400
1401	// Handles all the rendering of the public theme directory
1402	browse: function( section ) {
1403		// Create a new collection with the proper theme data
1404		// for each section
1405		this.collection.query( { browse: section } );
1406	},
1407
1408	// Sorting navigation
1409	onSort: function( event ) {
1410		var $el = $( event.target ),
1411			sort = $el.data( 'sort' );
1412
1413		event.preventDefault();
1414
1415		$( 'body' ).removeClass( 'filters-applied show-filters' );
1416
1417		// Bail if this is already active
1418		if ( $el.hasClass( this.activeClass ) ) {
1419			return;
1420		}
1421
1422		this.sort( sort );
1423
1424		// Trigger a router.naviagte update
1425		themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
1426	},
1427
1428	sort: function( sort ) {
1429		this.clearSearch();
1430
1431		$( '.filter-links li > a, .theme-filter' ).removeClass( this.activeClass );
1432		$( '[data-sort="' + sort + '"]' ).addClass( this.activeClass );
1433
1434		this.browse( sort );
1435	},
1436
1437	// Filters and Tags
1438	onFilter: function( event ) {
1439		var request,
1440			$el = $( event.target ),
1441			filter = $el.data( 'filter' );
1442
1443		// Bail if this is already active
1444		if ( $el.hasClass( this.activeClass ) ) {
1445			return;
1446		}
1447
1448		$( '.filter-links li > a, .theme-section' ).removeClass( this.activeClass );
1449		$el.addClass( this.activeClass );
1450
1451		if ( ! filter ) {
1452			return;
1453		}
1454
1455		// Construct the filter request
1456		// using the default values
1457		filter = _.union( filter, this.filtersChecked() );
1458		request = { tag: [ filter ] };
1459
1460		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1461		// or searching the local cache
1462		this.collection.query( request );
1463	},
1464
1465	// Clicking on a checkbox to add another filter to the request
1466	addFilter: function() {
1467		this.filtersChecked();
1468	},
1469
1470	// Applying filters triggers a tag request
1471	applyFilters: function( event ) {
1472		var name,
1473			tags = this.filtersChecked(),
1474			request = { tag: tags },
1475			filteringBy = $( '.filtered-by .tags' );
1476
1477		if ( event ) {
1478			event.preventDefault();
1479		}
1480
1481		$( 'body' ).addClass( 'filters-applied' );
1482		$( '.filter-links li > a.current' ).removeClass( 'current' );
1483		filteringBy.empty();
1484
1485		_.each( tags, function( tag ) {
1486			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
1487			filteringBy.append( '<span class="tag">' + name + '</span>' );
1488		});
1489
1490		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1491		// or searching the local cache
1492		this.collection.query( request );
1493	},
1494
1495	// Get the checked filters
1496	// @return {array} of tags or false
1497	filtersChecked: function() {
1498		var items = $( '.filter-group' ).find( ':checkbox' ),
1499			tags = [];
1500
1501		_.each( items.filter( ':checked' ), function( item ) {
1502			tags.push( $( item ).prop( 'value' ) );
1503		});
1504
1505		// When no filters are checked, restore initial state and return
1506		if ( tags.length === 0 ) {
1507			$( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
1508			$( '.filter-drawer .clear-filters' ).hide();
1509			$( 'body' ).removeClass( 'filters-applied' );
1510			return false;
1511		}
1512
1513		$( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
1514		$( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
1515
1516		return tags;
1517	},
1518
1519	activeClass: 'current',
1520
1521	// Overwrite search container class to append search
1522	// in new location
1523	searchContainer: $( '.wp-filter .search-form' ),
1524
1525	uploader: function() {
1526		$( 'a.upload' ).on( 'click', function( event ) {
1527			event.preventDefault();
1528			$( 'body' ).addClass( 'show-upload-theme' );
1529			themes.router.navigate( themes.router.baseUrl( '?upload' ), { replace: true } );
1530		});
1531		$( 'a.browse-themes' ).on( 'click', function( event ) {
1532			event.preventDefault();
1533			$( 'body' ).removeClass( 'show-upload-theme' );
1534			themes.router.navigate( themes.router.baseUrl( '' ), { replace: true } );
1535		});
1536	},
1537
1538	// Toggle the full filters navigation
1539	moreFilters: function( event ) {
1540		event.preventDefault();
1541
1542		if ( $( 'body' ).hasClass( 'filters-applied' ) ) {
1543			return this.backToFilters();
1544		}
1545
1546		// If the filters section is opened and filters are checked
1547		// run the relevant query collapsing to filtered-by state
1548		if ( $( 'body' ).hasClass( 'show-filters' ) && this.filtersChecked() ) {
1549			return this.addFilter();
1550		}
1551
1552		this.clearSearch();
1553
1554		themes.router.navigate( themes.router.baseUrl( '' ) );
1555		$( 'body' ).toggleClass( 'show-filters' );
1556	},
1557
1558	// Clears all the checked filters
1559	// @uses filtersChecked()
1560	clearFilters: function( event ) {
1561		var items = $( '.filter-group' ).find( ':checkbox' ),
1562			self = this;
1563
1564		event.preventDefault();
1565
1566		_.each( items.filter( ':checked' ), function( item ) {
1567			$( item ).prop( 'checked', false );
1568			return self.filtersChecked();
1569		});
1570	},
1571
1572	backToFilters: function( event ) {
1573		if ( event ) {
1574			event.preventDefault();
1575		}
1576
1577		$( 'body' ).removeClass( 'filters-applied' );
1578	},
1579
1580	clearSearch: function() {
1581		$( '#wp-filter-search-input').val( '' );
1582	}
1583});
1584
1585themes.InstallerRouter = Backbone.Router.extend({
1586	routes: {
1587		'theme-install.php?theme=:slug': 'preview',
1588		'theme-install.php?browse=:sort': 'sort',
1589		'theme-install.php?upload': 'upload',
1590		'theme-install.php?search=:query': 'search',
1591		'theme-install.php': 'sort'
1592	},
1593
1594	baseUrl: function( url ) {
1595		return 'theme-install.php' + url;
1596	},
1597
1598	themePath: '?theme=',
1599	browsePath: '?browse=',
1600	searchPath: '?search=',
1601
1602	search: function( query ) {
1603		$( '.wp-filter-search' ).val( query );
1604	},
1605
1606	navigate: function() {
1607		if ( Backbone.history._hasPushState ) {
1608			Backbone.Router.prototype.navigate.apply( this, arguments );
1609		}
1610	}
1611});
1612
1613
1614themes.RunInstaller = {
1615
1616	init: function() {
1617		// Set up the view
1618		// Passes the default 'section' as an option
1619		this.view = new themes.view.Installer({
1620			section: 'featured',
1621			SearchView: themes.view.InstallerSearch
1622		});
1623
1624		// Render results
1625		this.render();
1626
1627	},
1628
1629	render: function() {
1630
1631		// Render results
1632		this.view.render();
1633		this.routes();
1634
1635		Backbone.history.start({
1636			root: themes.data.settings.adminUrl,
1637			pushState: true,
1638			hashChange: false
1639		});
1640	},
1641
1642	routes: function() {
1643		var self = this,
1644			request = {};
1645
1646		// Bind to our global `wp.themes` object
1647		// so that the router is available to sub-views
1648		themes.router = new themes.InstallerRouter();
1649
1650		// Handles `theme` route event
1651		// Queries the API for the passed theme slug
1652		themes.router.on( 'route:preview', function( slug ) {
1653			request.theme = slug;
1654			self.view.collection.query( request );
1655		});
1656
1657		// Handles sorting / browsing routes
1658		// Also handles the root URL triggering a sort request
1659		// for `featured`, the default view
1660		themes.router.on( 'route:sort', function( sort ) {
1661			if ( ! sort ) {
1662				sort = 'featured';
1663			}
1664			self.view.sort( sort );
1665			self.view.trigger( 'theme:close' );
1666		});
1667
1668		// Support the `upload` route by going straight to upload section
1669		themes.router.on( 'route:upload', function() {
1670			$( 'a.upload' ).trigger( 'click' );
1671		});
1672
1673		// The `search` route event. The router populates the input field.
1674		themes.router.on( 'route:search', function() {
1675			$( '.wp-filter-search' ).focus().trigger( 'keyup' );
1676		});
1677
1678		this.extraRoutes();
1679	},
1680
1681	extraRoutes: function() {
1682		return false;
1683	}
1684};
1685
1686// Ready...
1687$( document ).ready(function() {
1688	if ( themes.isInstall ) {
1689		themes.RunInstaller.init();
1690	} else {
1691		themes.Run.init();
1692	}
1693
1694	$( '.broken-themes .delete-theme' ).on( 'click', function() {
1695		return confirm( _wpThemeSettings.settings.confirmDelete );
1696	});
1697});
1698
1699})( jQuery );
1700
1701// Align theme browser thickbox
1702var tb_position;
1703jQuery(document).ready( function($) {
1704	tb_position = function() {
1705		var tbWindow = $('#TB_window'),
1706			width = $(window).width(),
1707			H = $(window).height(),
1708			W = ( 1040 < width ) ? 1040 : width,
1709			adminbar_height = 0;
1710
1711		if ( $('#wpadminbar').length ) {
1712			adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
1713		}
1714
1715		if ( tbWindow.size() ) {
1716			tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
1717			$('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
1718			tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
1719			if ( typeof document.body.style.maxWidth !== 'undefined' ) {
1720				tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
1721			}
1722		}
1723	};
1724
1725	$(window).resize(function(){ tb_position(); });
1726});