PageRenderTime 81ms CodeModel.GetById 3ms app.highlight 65ms RepoModel.GetById 2ms app.codeStats 0ms

/wp-admin/js/theme.js

https://gitlab.com/savaf/WordPress
JavaScript | 1707 lines | 1022 code | 358 blank | 327 comment | 124 complexity | 390ff3932274834922bdd0aff9b09257 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.find( '.themes' ).remove();
  83		this.$el.append( this.view.el ).addClass( 'rendered' );
  84		this.$el.append( '<br class="clear"/>' );
  85	},
  86
  87	// Defines search element container
  88	searchContainer: $( '#wpbody h2:first' ),
  89
  90	// Search input and view
  91	// for current theme collection
  92	search: function() {
  93		var view,
  94			self = this;
  95
  96		// Don't render the search if there is only one theme
  97		if ( themes.data.themes.length === 1 ) {
  98			return;
  99		}
 100
 101		view = new this.SearchView({
 102			collection: self.collection,
 103			parent: this
 104		});
 105
 106		// Render and append after screen title
 107		view.render();
 108		this.searchContainer
 109			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
 110			.append( view.el );
 111	},
 112
 113	// Checks when the user gets close to the bottom
 114	// of the mage and triggers a theme:scroll event
 115	scroller: function() {
 116		var self = this,
 117			bottom, threshold;
 118
 119		bottom = this.window.scrollTop() + self.window.height();
 120		threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
 121		threshold = Math.round( threshold * 0.9 );
 122
 123		if ( bottom > threshold ) {
 124			this.trigger( 'theme:scroll' );
 125		}
 126	}
 127});
 128
 129// Set up the Collection for our theme data
 130// @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
 131themes.Collection = Backbone.Collection.extend({
 132
 133	model: themes.Model,
 134
 135	// Search terms
 136	terms: '',
 137
 138	// Controls searching on the current theme collection
 139	// and triggers an update event
 140	doSearch: function( value ) {
 141
 142		// Don't do anything if we've already done this search
 143		// Useful because the Search handler fires multiple times per keystroke
 144		if ( this.terms === value ) {
 145			return;
 146		}
 147
 148		// Updates terms with the value passed
 149		this.terms = value;
 150
 151		// If we have terms, run a search...
 152		if ( this.terms.length > 0 ) {
 153			this.search( this.terms );
 154		}
 155
 156		// If search is blank, show all themes
 157		// Useful for resetting the views when you clean the input
 158		if ( this.terms === '' ) {
 159			this.reset( themes.data.themes );
 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	initialize: function( options ) {
 835		var self = this;
 836
 837		// Set up parent
 838		this.parent = options.parent;
 839
 840		// Set current view to [grid]
 841		this.setView( 'grid' );
 842
 843		// Move the active theme to the beginning of the collection
 844		self.currentTheme();
 845
 846		// When the collection is updated by user input...
 847		this.listenTo( self.collection, 'update', function() {
 848			self.parent.page = 0;
 849			self.currentTheme();
 850			self.render( this );
 851		});
 852
 853		// Update theme count to full result set when available.
 854		this.listenTo( self.collection, 'query:success', function( count ) {
 855			if ( _.isNumber( count ) ) {
 856				self.count.text( count );
 857			} else {
 858				self.count.text( self.collection.length );
 859			}
 860		});
 861
 862		this.listenTo( self.collection, 'query:empty', function() {
 863			$( 'body' ).addClass( 'no-results' );
 864		});
 865
 866		this.listenTo( this.parent, 'theme:scroll', function() {
 867			self.renderThemes( self.parent.page );
 868		});
 869
 870		this.listenTo( this.parent, 'theme:close', function() {
 871			if ( self.overlay ) {
 872				self.overlay.closeOverlay();
 873			}
 874		} );
 875
 876		// Bind keyboard events.
 877		$( 'body' ).on( 'keyup', function( event ) {
 878			if ( ! self.overlay ) {
 879				return;
 880			}
 881
 882			// Pressing the right arrow key fires a theme:next event
 883			if ( event.keyCode === 39 ) {
 884				self.overlay.nextTheme();
 885			}
 886
 887			// Pressing the left arrow key fires a theme:previous event
 888			if ( event.keyCode === 37 ) {
 889				self.overlay.previousTheme();
 890			}
 891
 892			// Pressing the escape key fires a theme:collapse event
 893			if ( event.keyCode === 27 ) {
 894				self.overlay.collapse( event );
 895			}
 896		});
 897	},
 898
 899	// Manages rendering of theme pages
 900	// and keeping theme count in sync
 901	render: function() {
 902		// Clear the DOM, please
 903		this.$el.html( '' );
 904
 905		// If the user doesn't have switch capabilities
 906		// or there is only one theme in the collection
 907		// render the detailed view of the active theme
 908		if ( themes.data.themes.length === 1 ) {
 909
 910			// Constructs the view
 911			this.singleTheme = new themes.view.Details({
 912				model: this.collection.models[0]
 913			});
 914
 915			// Render and apply a 'single-theme' class to our container
 916			this.singleTheme.render();
 917			this.$el.addClass( 'single-theme' );
 918			this.$el.append( this.singleTheme.el );
 919		}
 920
 921		// Generate the themes
 922		// Using page instance
 923		// While checking the collection has items
 924		if ( this.options.collection.size() > 0 ) {
 925			this.renderThemes( this.parent.page );
 926		}
 927
 928		// Display a live theme count for the collection
 929		this.count.text( this.collection.count ? this.collection.count : this.collection.length );
 930	},
 931
 932	// Iterates through each instance of the collection
 933	// and renders each theme module
 934	renderThemes: function( page ) {
 935		var self = this;
 936
 937		self.instance = self.collection.paginate( page );
 938
 939		// If we have no more themes bail
 940		if ( self.instance.size() === 0 ) {
 941			// Fire a no-more-themes event.
 942			this.parent.trigger( 'theme:end' );
 943			return;
 944		}
 945
 946		// Make sure the add-new stays at the end
 947		if ( page >= 1 ) {
 948			$( '.add-new-theme' ).remove();
 949		}
 950
 951		// Loop through the themes and setup each theme view
 952		self.instance.each( function( theme ) {
 953			self.theme = new themes.view.Theme({
 954				model: theme,
 955				parent: self
 956			});
 957
 958			// Render the views...
 959			self.theme.render();
 960			// and append them to div.themes
 961			self.$el.append( self.theme.el );
 962
 963			// Binds to theme:expand to show the modal box
 964			// with the theme details
 965			self.listenTo( self.theme, 'theme:expand', self.expand, self );
 966		});
 967
 968		// 'Add new theme' element shown at the end of the grid
 969		if ( themes.data.settings.canInstall ) {
 970			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>' );
 971		}
 972
 973		this.parent.page++;
 974	},
 975
 976	// Grabs current theme and puts it at the beginning of the collection
 977	currentTheme: function() {
 978		var self = this,
 979			current;
 980
 981		current = self.collection.findWhere({ active: true });
 982
 983		// Move the active theme to the beginning of the collection
 984		if ( current ) {
 985			self.collection.remove( current );
 986			self.collection.add( current, { at:0 } );
 987		}
 988	},
 989
 990	// Sets current view
 991	setView: function( view ) {
 992		return view;
 993	},
 994
 995	// Renders the overlay with the ThemeDetails view
 996	// Uses the current model data
 997	expand: function( id ) {
 998		var self = this;
 999
1000		// Set the current theme model
1001		this.model = self.collection.get( id );
1002
1003		// Trigger a route update for the current model
1004		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
1005
1006		// Sets this.view to 'detail'
1007		this.setView( 'detail' );
1008		$( 'body' ).addClass( 'modal-open' );
1009
1010		// Set up the theme details view
1011		this.overlay = new themes.view.Details({
1012			model: self.model
1013		});
1014
1015		this.overlay.render();
1016		this.$overlay.html( this.overlay.el );
1017
1018		// Bind to theme:next and theme:previous
1019		// triggered by the arrow keys
1020		//
1021		// Keep track of the current model so we
1022		// can infer an index position
1023		this.listenTo( this.overlay, 'theme:next', function() {
1024			// Renders the next theme on the overlay
1025			self.next( [ self.model.cid ] );
1026
1027		})
1028		.listenTo( this.overlay, 'theme:previous', function() {
1029			// Renders the previous theme on the overlay
1030			self.previous( [ self.model.cid ] );
1031		});
1032	},
1033
1034	// This method renders the next theme on the overlay modal
1035	// based on the current position in the collection
1036	// @params [model cid]
1037	next: function( args ) {
1038		var self = this,
1039			model, nextModel;
1040
1041		// Get the current theme
1042		model = self.collection.get( args[0] );
1043		// Find the next model within the collection
1044		nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
1045
1046		// Sanity check which also serves as a boundary test
1047		if ( nextModel !== undefined ) {
1048
1049			// We have a new theme...
1050			// Close the overlay
1051			this.overlay.closeOverlay();
1052
1053			// Trigger a route update for the current model
1054			self.theme.trigger( 'theme:expand', nextModel.cid );
1055
1056		}
1057	},
1058
1059	// This method renders the previous theme on the overlay modal
1060	// based on the current position in the collection
1061	// @params [model cid]
1062	previous: function( args ) {
1063		var self = this,
1064			model, previousModel;
1065
1066		// Get the current theme
1067		model = self.collection.get( args[0] );
1068		// Find the previous model within the collection
1069		previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
1070
1071		if ( previousModel !== undefined ) {
1072
1073			// We have a new theme...
1074			// Close the overlay
1075			this.overlay.closeOverlay();
1076
1077			// Trigger a route update for the current model
1078			self.theme.trigger( 'theme:expand', previousModel.cid );
1079
1080		}
1081	}
1082});
1083
1084// Search input view controller.
1085themes.view.Search = wp.Backbone.View.extend({
1086
1087	tagName: 'input',
1088	className: 'wp-filter-search',
1089	id: 'wp-filter-search-input',
1090	searching: false,
1091
1092	attributes: {
1093		placeholder: l10n.searchPlaceholder,
1094		type: 'search'
1095	},
1096
1097	events: {
1098		'input':  'search',
1099		'keyup':  'search',
1100		'change': 'search',
1101		'search': 'search',
1102		'blur':   'pushState'
1103	},
1104
1105	initialize: function( options ) {
1106
1107		this.parent = options.parent;
1108
1109		this.listenTo( this.parent, 'theme:close', function() {
1110			this.searching = false;
1111		} );
1112
1113	},
1114
1115	// Runs a search on the theme collection.
1116	search: function( event ) {
1117		var options = {};
1118
1119		// Clear on escape.
1120		if ( event.type === 'keyup' && event.which === 27 ) {
1121			event.target.value = '';
1122		}
1123
1124		// Lose input focus when pressing enter
1125		if ( event.which === 13 ) {
1126			this.$el.trigger( 'blur' );
1127		}
1128
1129		this.collection.doSearch( event.target.value );
1130
1131		// if search is initiated and key is not return
1132		if ( this.searching && event.which !== 13 ) {
1133			options.replace = true;
1134		} else {
1135			this.searching = true;
1136		}
1137
1138		// Update the URL hash
1139		if ( event.target.value ) {
1140			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
1141		} else {
1142			themes.router.navigate( themes.router.baseUrl( '' ) );
1143		}
1144	},
1145
1146	pushState: function( event ) {
1147		var url = themes.router.baseUrl( '' );
1148
1149		if ( event.target.value ) {
1150			url = themes.router.baseUrl( themes.router.searchPath + event.target.value );
1151		}
1152
1153		this.searching = false;
1154		themes.router.navigate( url );
1155
1156	}
1157});
1158
1159// Sets up the routes events for relevant url queries
1160// Listens to [theme] and [search] params
1161themes.Router = Backbone.Router.extend({
1162
1163	routes: {
1164		'themes.php?theme=:slug': 'theme',
1165		'themes.php?search=:query': 'search',
1166		'themes.php?s=:query': 'search',
1167		'themes.php': 'themes',
1168		'': 'themes'
1169	},
1170
1171	baseUrl: function( url ) {
1172		return 'themes.php' + url;
1173	},
1174
1175	themePath: '?theme=',
1176	searchPath: '?search=',
1177
1178	search: function( query ) {
1179		$( '.wp-filter-search' ).val( query );
1180	},
1181
1182	themes: function() {
1183		$( '.wp-filter-search' ).val( '' );
1184	},
1185
1186	navigate: function() {
1187		if ( Backbone.history._hasPushState ) {
1188			Backbone.Router.prototype.navigate.apply( this, arguments );
1189		}
1190	}
1191
1192});
1193
1194// Execute and setup the application
1195themes.Run = {
1196	init: function() {
1197		// Initializes the blog's theme library view
1198		// Create a new collection with data
1199		this.themes = new themes.Collection( themes.data.themes );
1200
1201		// Set up the view
1202		this.view = new themes.view.Appearance({
1203			collection: this.themes
1204		});
1205
1206		this.render();
1207	},
1208
1209	render: function() {
1210
1211		// Render results
1212		this.view.render();
1213		this.routes();
1214
1215		Backbone.history.start({
1216			root: themes.data.settings.adminUrl,
1217			pushState: true,
1218			hashChange: false
1219		});
1220	},
1221
1222	routes: function() {
1223		var self = this;
1224		// Bind to our global thx object
1225		// so that the object is available to sub-views
1226		themes.router = new themes.Router();
1227
1228		// Handles theme details route event
1229		themes.router.on( 'route:theme', function( slug ) {
1230			self.view.view.expand( slug );
1231		});
1232
1233		themes.router.on( 'route:themes', function() {
1234			self.themes.doSearch( '' );
1235			self.view.trigger( 'theme:close' );
1236		});
1237
1238		// Handles search route event
1239		themes.router.on( 'route:search', function() {
1240			$( '.wp-filter-search' ).trigger( 'keyup' );
1241		});
1242
1243		this.extraRoutes();
1244	},
1245
1246	extraRoutes: function() {
1247		return false;
1248	}
1249};
1250
1251// Extend the main Search view
1252themes.view.InstallerSearch =  themes.view.Search.extend({
1253
1254	events: {
1255		'keyup': 'search'
1256	},
1257
1258	// Handles Ajax request for searching through themes in public repo
1259	search: function( event ) {
1260
1261		// Tabbing or reverse tabbing into the search input shouldn't trigger a search
1262		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
1263			return;
1264		}
1265
1266		this.collection = this.options.parent.view.collection;
1267
1268		// Clear on escape.
1269		if ( event.type === 'keyup' && event.which === 27 ) {
1270			event.target.value = '';
1271		}
1272
1273		_.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value );
1274	},
1275
1276	doSearch: _.debounce( function( value ) {
1277		var request = {};
1278
1279		request.search = value;
1280
1281		// Intercept an [author] search.
1282		//
1283		// If input value starts with `author:` send a request
1284		// for `author` instead of a regular `search`
1285		if ( value.substring( 0, 7 ) === 'author:' ) {
1286			request.search = '';
1287			request.author = value.slice( 7 );
1288		}
1289
1290		// Intercept a [tag] search.
1291		//
1292		// If input value starts with `tag:` send a request
1293		// for `tag` instead of a regular `search`
1294		if ( value.substring( 0, 4 ) === 'tag:' ) {
1295			request.search = '';
1296			request.tag = [ value.slice( 4 ) ];
1297		}
1298
1299		$( '.filter-links li > a.current' ).removeClass( 'current' );
1300		$( 'body' ).removeClass( 'show-filters filters-applied' );
1301
1302		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1303		// or searching the local cache
1304		this.collection.query( request );
1305
1306		// Set route
1307		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + value ), { replace: true } );
1308	}, 300 )
1309});
1310
1311themes.view.Installer = themes.view.Appearance.extend({
1312
1313	el: '#wpbody-content .wrap',
1314
1315	// Register events for sorting and filters in theme-navigation
1316	events: {
1317		'click .filter-links li > a': 'onSort',
1318		'click .theme-filter': 'onFilter',
1319		'click .drawer-toggle': 'moreFilters',
1320		'click .filter-drawer .apply-filters': 'applyFilters',
1321		'click .filter-group [type="checkbox"]': 'addFilter',
1322		'click .filter-drawer .clear-filters': 'clearFilters',
1323		'click .filtered-by': 'backToFilters'
1324	},
1325
1326	// Initial render method
1327	render: function() {
1328		var self = this;
1329
1330		this.search();
1331		this.uploader();
1332
1333		this.collection = new themes.Collection();
1334
1335		// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
1336		this.listenTo( this, 'theme:end', function() {
1337
1338			// Make sure we are not already loading
1339			if ( self.collection.loadingThemes ) {
1340				return;
1341			}
1342
1343			// Set loadingThemes to true and bump page instance of currentQuery.
1344			self.collection.loadingThemes = true;
1345			self.collection.currentQuery.page++;
1346
1347			// Use currentQuery.page to build the themes request.
1348			_.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
1349			self.collection.query( self.collection.currentQuery.request );
1350		});
1351
1352		this.listenTo( this.collection, 'query:success', function() {
1353			$( 'body' ).removeClass( 'loading-content' );
1354			$( '.theme-browser' ).find( 'div.error' ).remove();
1355		});
1356
1357		this.listenTo( this.collection, 'query:fail', function() {
1358			$( 'body' ).removeClass( 'loading-content' );
1359			$( '.theme-browser' ).find( 'div.error' ).remove();
1360			$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p></div>' );
1361		});
1362
1363		if ( this.view ) {
1364			this.view.remove();
1365		}
1366
1367		// Set ups the view and passes the section argument
1368		this.view = new themes.view.Themes({
1369			collection: this.collection,
1370			parent: this
1371		});
1372
1373		// Reset pagination every time the install view handler is run
1374		this.page = 0;
1375
1376		// Render and append
1377		this.$el.find( '.themes' ).remove();
1378		this.view.render();
1379		this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
1380	},
1381
1382	// Handles all the rendering of the public theme directory
1383	browse: function( section ) {
1384		// Create a new collection with the proper theme data
1385		// for each section
1386		this.collection.query( { browse: section } );
1387	},
1388
1389	// Sorting navigation
1390	onSort: function( event ) {
1391		var $el = $( event.target ),
1392			sort = $el.data( 'sort' );
1393
1394		event.preventDefault();
1395
1396		$( 'body' ).removeClass( 'filters-applied show-filters' );
1397
1398		// Bail if this is already active
1399		if ( $el.hasClass( this.activeClass ) ) {
1400			return;
1401		}
1402
1403		this.sort( sort );
1404
1405		// Trigger a router.naviagte update
1406		themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
1407	},
1408
1409	sort: function( sort ) {
1410		this.clearSearch();
1411
1412		$( '.filter-links li > a, .theme-filter' ).removeClass( this.activeClass );
1413		$( '[data-sort="' + sort + '"]' ).addClass( this.activeClass );
1414
1415		this.browse( sort );
1416	},
1417
1418	// Filters and Tags
1419	onFilter: function( event ) {
1420		var request,
1421			$el = $( event.target ),
1422			filter = $el.data( 'filter' );
1423
1424		// Bail if this is already active
1425		if ( $el.hasClass( this.activeClass ) ) {
1426			return;
1427		}
1428
1429		$( '.filter-links li > a, .theme-section' ).removeClass( this.activeClass );
1430		$el.addClass( this.activeClass );
1431
1432		if ( ! filter ) {
1433			return;
1434		}
1435
1436		// Construct the filter request
1437		// using the default values
1438		filter = _.union( filter, this.filtersChecked() );
1439		request = { tag: [ filter ] };
1440
1441		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1442		// or searching the local cache
1443		this.collection.query( request );
1444	},
1445
1446	// Clicking on a checkbox to add another filter to the request
1447	addFilter: function() {
1448		this.filtersChecked();
1449	},
1450
1451	// Applying filters triggers a tag request
1452	applyFilters: function( event ) {
1453		var name,
1454			tags = this.filtersChecked(),
1455			request = { tag: tags },
1456			filteringBy = $( '.filtered-by .tags' );
1457
1458		if ( event ) {
1459			event.preventDefault();
1460		}
1461
1462		$( 'body' ).addClass( 'filters-applied' );
1463		$( '.filter-links li > a.current' ).removeClass( 'current' );
1464		filteringBy.empty();
1465
1466		_.each( tags, function( tag ) {
1467			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
1468			filteringBy.append( '<span class="tag">' + name + '</span>' );
1469		});
1470
1471		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
1472		// or searching the local cache
1473		this.collection.query( request );
1474	},
1475
1476	// Get the checked filters
1477	// @return {array} of tags or false
1478	filtersChecked: function() {
1479		var items = $( '.filter-group' ).find( ':checkbox' ),
1480			tags = [];
1481
1482		_.each( items.filter( ':checked' ), function( item ) {
1483			tags.push( $( item ).prop( 'value' ) );
1484		});
1485
1486		// When no filters are checked, restore initial state and return
1487		if ( tags.length === 0 ) {
1488			$( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
1489			$( '.filter-drawer .clear-filters' ).hide();
1490			$( 'body' ).removeClass( 'filters-applied' );
1491			return false;
1492		}
1493
1494		$( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
1495		$( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
1496
1497		return tags;
1498	},
1499
1500	activeClass: 'current',
1501
1502	// Overwrite search container class to append search
1503	// in new location
1504	searchContainer: $( '.wp-filter .search-form' ),
1505
1506	uploader: function() {
1507		$( 'a.upload' ).on( 'click', function( event ) {
1508			event.preventDefault();
1509			$( 'body' ).addClass( 'show-upload-theme' );
1510			themes.router.navigate( themes.router.baseUrl( '?upload' ), { replace: true } );
1511		});
1512		$( 'a.browse-themes' ).on( 'click', function( event ) {
1513			event.preventDefault();
1514			$( 'body' ).removeClass( 'show-upload-theme' );
1515			themes.router.navigate( themes.router.baseUrl( '' ), { replace: true } );
1516		});
1517	},
1518
1519	// Toggle the full filters navigation
1520	moreFilters: function( event ) {
1521		event.preventDefault();
1522
1523		if ( $( 'body' ).hasClass( 'filters-applied' ) ) {
1524			return this.backToFilters();
1525		}
1526
1527		// If the filters section is opened and filters are checked
1528		// run the relevant query collapsing to filtered-by state
1529		if ( $( 'body' ).hasClass( 'show-filters' ) && this.filtersChecked() ) {
1530			return this.addFilter();
1531		}
1532
1533		this.clearSearch();
1534
1535		themes.router.navigate( themes.router.baseUrl( '' ) );
1536		$( 'body' ).toggleClass( 'show-filters' );
1537	},
1538
1539	// Clears all the checked filters
1540	// @uses filtersChecked()
1541	clearFilters: function( event ) {
1542		var items = $( '.filter-group' ).find( ':checkbox' ),
1543			self = this;
1544
1545		event.preventDefault();
1546
1547		_.each( items.filter( ':checked' ), function( item ) {
1548			$( item ).prop( 'checked', false );
1549			return self.filtersChecked();
1550		});
1551	},
1552
1553	backToFilters: function( event ) {
1554		if ( event ) {
1555			event.preventDefault();
1556		}
1557
1558		$( 'body' ).removeClass( 'filters-applied' );
1559	},
1560
1561	clearSearch: function() {
1562		$( '#wp-filter-search-input').val( '' );
1563	}
1564});
1565
1566themes.InstallerRouter = Backbone.Router.extend({
1567	routes: {
1568		'theme-install.php?theme=:slug': 'preview',
1569		'theme-install.php?browse=:sort': 'sort',
1570		'theme-install.php?upload': 'upload',
1571		'theme-install.php?search=:query': 'search',
1572		'theme-install.php': 'sort'
1573	},
1574
1575	baseUrl: function( url ) {
1576		return 'theme-install.php' + url;
1577	},
1578
1579	themePath: '?theme=',
1580	browsePath: '?browse=',
1581	searchPath: '?search=',
1582
1583	search: function( query ) {
1584		$( '.wp-filter-search' ).val( query );
1585	},
1586
1587	navigate: function() {
1588		if ( Backbone.history._hasPushState ) {
1589			Backbone.Router.prototype.navigate.apply( this, arguments );
1590		}
1591	}
1592});
1593
1594
1595themes.RunInstaller = {
1596
1597	init: function() {
1598		// Set up the view
1599		// Passes the default 'section' as an option
1600		this.view = new themes.view.Installer({
1601			section: 'featured',
1602			SearchView: themes.view.InstallerSearch
1603		});
1604
1605		// Render results
1606		this.render();
1607
1608	},
1609
1610	render: function() {
1611
1612		// Render results
1613		this.view.render();
1614		this.routes();
1615
1616		Backbone.history.start({
1617			root: themes.data.settings.adminUrl,
1618			pushState: true,
1619			hashChange: false
1620		});
1621	},
1622
1623	routes: function() {
1624		var self = this,
1625			request = {};
1626
1627		// Bind to our global `wp.themes` object
1628		// so that the router is available to sub-views
1629		themes.router = new themes.InstallerRouter();
1630
1631		// Handles `theme` route event
1632		// Queries the API for the passed theme slug
1633		themes.router.on( 'route:preview', function( slug ) {
1634			request.theme = slug;
1635			self.view.collection.query( request );
1636		});
1637
1638		// Handles sorting / browsing routes
1639		// Also handles the root URL triggering a sort request
1640		// for `featured`, the default view
1641		themes.router.on( 'route:sort', function( sort ) {
1642			if ( ! sort ) {
1643				sort = 'featured';
1644			}
1645			self.view.sort( sort );
1646			self.view.trigger( 'theme:close' );
1647		});
1648
1649		// Support the `upload` route by going straight to upload section
1650		themes.router.on( 'route:upload', function() {
1651			$( 'a.upload' ).trigger( 'click' );
1652		});
1653
1654		// The `search` route event. The router populates the input field.
1655		themes.router.on( 'route:search', function() {
1656			$( '.wp-filter-search' ).focus().trigger( 'keyup' );
1657		});
1658
1659		this.extraRoutes();
1660	},
1661
1662	extraRoutes: function() {
1663		return false;
1664	}
1665};
1666
1667// Ready...
1668$( document ).ready(function() {
1669	if ( themes.isInstall ) {
1670		themes.RunInstaller.init();
1671	} else {
1672		themes.Run.init();
1673	}
1674
1675	$( '.broken-themes .delete-theme' ).on( 'click', function() {
1676		return confirm( _wpThemeSettings.settings.confirmDelete );
1677	});
1678});
1679
1680})( jQuery );
1681
1682// Align theme browser thickbox
1683var tb_position;
1684jQuery(document).ready( function($) {
1685	tb_position = function() {
1686		var tbWindow = $('#TB_window'),
1687			width = $(window).width(),
1688			H = $(window).height(),
1689			W = ( 1040 < width ) ? 1040 : width,
1690			adminbar_height = 0;
1691
1692		if ( $('#wpadminbar').length ) {
1693			adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
1694		}
1695
1696		if ( tbWindow.size() ) {
1697			tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
1698			$('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
1699			tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
1700			if ( typeof document.body.style.maxWidth !== 'undefined' ) {
1701				tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
1702			}
1703		}
1704	};
1705
1706	$(window).resize(function(){ tb_position(); });
1707});