PageRenderTime 74ms CodeModel.GetById 3ms app.highlight 59ms RepoModel.GetById 1ms app.codeStats 0ms

/wp/wp-admin/js/theme.js

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