PageRenderTime 83ms CodeModel.GetById 3ms app.highlight 67ms RepoModel.GetById 1ms app.codeStats 1ms

/wp-admin/js/theme.js

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