PageRenderTime 83ms CodeModel.GetById 29ms app.highlight 41ms RepoModel.GetById 0ms app.codeStats 1ms

/wp-admin/js/theme.js

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