wp/wp-admin/js/theme.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
    75 		});
    75 		});
    76 
    76 
    77 		// Render search form.
    77 		// Render search form.
    78 		this.search();
    78 		this.search();
    79 
    79 
       
    80 		this.$el.removeClass( 'search-loading' );
       
    81 
    80 		// Render and append
    82 		// Render and append
    81 		this.view.render();
    83 		this.view.render();
    82 		this.$el.empty().append( this.view.el ).addClass( 'rendered' );
    84 		this.$el.empty().append( this.view.el ).addClass( 'rendered' );
    83 		this.$el.append( '<br class="clear"/>' );
       
    84 	},
    85 	},
    85 
    86 
    86 	// Defines search element container
    87 	// Defines search element container
    87 	searchContainer: $( '#wpbody h2:first' ),
    88 	searchContainer: $( '.search-form' ),
    88 
    89 
    89 	// Search input and view
    90 	// Search input and view
    90 	// for current theme collection
    91 	// for current theme collection
    91 	search: function() {
    92 	search: function() {
    92 		var view,
    93 		var view,
    99 
   100 
   100 		view = new this.SearchView({
   101 		view = new this.SearchView({
   101 			collection: self.collection,
   102 			collection: self.collection,
   102 			parent: this
   103 			parent: this
   103 		});
   104 		});
       
   105 		self.SearchView = view;
   104 
   106 
   105 		// Render and append after screen title
   107 		// Render and append after screen title
   106 		view.render();
   108 		view.render();
   107 		this.searchContainer
   109 		this.searchContainer
   108 			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
   110 			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
   109 			.append( view.el );
   111 			.append( view.el )
       
   112 			.on( 'submit', function( event ) {
       
   113 				event.preventDefault();
       
   114 			});
   110 	},
   115 	},
   111 
   116 
   112 	// Checks when the user gets close to the bottom
   117 	// Checks when the user gets close to the bottom
   113 	// of the mage and triggers a theme:scroll event
   118 	// of the mage and triggers a theme:scroll event
   114 	scroller: function() {
   119 	scroller: function() {
   157 		if ( this.terms === '' ) {
   162 		if ( this.terms === '' ) {
   158 			this.reset( themes.data.themes );
   163 			this.reset( themes.data.themes );
   159 			$( 'body' ).removeClass( 'no-results' );
   164 			$( 'body' ).removeClass( 'no-results' );
   160 		}
   165 		}
   161 
   166 
   162 		// Trigger an 'update' event
   167 		// Trigger a 'themes:update' event
   163 		this.trigger( 'update' );
   168 		this.trigger( 'themes:update' );
   164 	},
   169 	},
   165 
   170 
   166 	// Performs a search within the collection
   171 	// Performs a search within the collection
   167 	// @uses RegExp
   172 	// @uses RegExp
   168 	search: function( term ) {
   173 	search: function( term ) {
   184 		results = this.filter( function( data ) {
   189 		results = this.filter( function( data ) {
   185 			name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
   190 			name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
   186 			description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
   191 			description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
   187 			author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
   192 			author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
   188 
   193 
   189 			haystack = _.union( name, data.get( 'id' ), description, author, data.get( 'tags' ) );
   194 			haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );
   190 
   195 
   191 			if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
   196 			if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
   192 				data.set( 'displayAuthor', true );
   197 				data.set( 'displayAuthor', true );
   193 			}
   198 			}
   194 
   199 
   263 					queries.push( { themes: data.themes, request: request, total: count } );
   268 					queries.push( { themes: data.themes, request: request, total: count } );
   264 				}
   269 				}
   265 
   270 
   266 				// Trigger a collection refresh event
   271 				// Trigger a collection refresh event
   267 				// and a `query:success` event with a `count` argument.
   272 				// and a `query:success` event with a `count` argument.
   268 				self.trigger( 'update' );
   273 				self.trigger( 'themes:update' );
   269 				self.trigger( 'query:success', count );
   274 				self.trigger( 'query:success', count );
   270 
   275 
   271 				if ( data.themes && data.themes.length === 0 ) {
   276 				if ( data.themes && data.themes.length === 0 ) {
   272 					self.trigger( 'query:empty' );
   277 					self.trigger( 'query:empty' );
   273 				}
   278 				}
   307 			this.reset( query.themes );
   312 			this.reset( query.themes );
   308 			if ( ! query.total ) {
   313 			if ( ! query.total ) {
   309 				this.count = this.length;
   314 				this.count = this.length;
   310 			}
   315 			}
   311 
   316 
   312 			this.trigger( 'update' );
   317 			this.trigger( 'themes:update' );
   313 			this.trigger( 'query:success', this.count );
   318 			this.trigger( 'query:success', this.count );
   314 		}
   319 		}
   315 	},
   320 	},
   316 
   321 
   317 	// Local cache array for API queries
   322 	// Local cache array for API queries
   374 	events: {
   379 	events: {
   375 		'click': themes.isInstall ? 'preview': 'expand',
   380 		'click': themes.isInstall ? 'preview': 'expand',
   376 		'keydown': themes.isInstall ? 'preview': 'expand',
   381 		'keydown': themes.isInstall ? 'preview': 'expand',
   377 		'touchend': themes.isInstall ? 'preview': 'expand',
   382 		'touchend': themes.isInstall ? 'preview': 'expand',
   378 		'keyup': 'addFocus',
   383 		'keyup': 'addFocus',
   379 		'touchmove': 'preventExpand'
   384 		'touchmove': 'preventExpand',
       
   385 		'click .theme-install': 'installTheme',
       
   386 		'click .update-message': 'updateTheme'
   380 	},
   387 	},
   381 
   388 
   382 	touchDrag: false,
   389 	touchDrag: false,
       
   390 
       
   391 	initialize: function() {
       
   392 		this.model.on( 'change', this.render, this );
       
   393 	},
   383 
   394 
   384 	render: function() {
   395 	render: function() {
   385 		var data = this.model.toJSON();
   396 		var data = this.model.toJSON();
       
   397 
   386 		// Render themes using the html template
   398 		// Render themes using the html template
   387 		this.$el.html( this.html( data ) ).attr({
   399 		this.$el.html( this.html( data ) ).attr({
   388 			tabindex: 0,
   400 			tabindex: 0,
   389 			'aria-describedby' : data.id + '-action ' + data.id + '-name'
   401 			'aria-describedby' : data.id + '-action ' + data.id + '-name',
       
   402 			'data-slug': data.id
   390 		});
   403 		});
   391 
   404 
   392 		// Renders active theme styles
   405 		// Renders active theme styles
   393 		this.activeTheme();
   406 		this.activeTheme();
   394 
   407 
   395 		if ( this.model.get( 'displayAuthor' ) ) {
   408 		if ( this.model.get( 'displayAuthor' ) ) {
   396 			this.$el.addClass( 'display-author' );
   409 			this.$el.addClass( 'display-author' );
   397 		}
       
   398 
       
   399 		if ( this.model.get( 'installed' ) ) {
       
   400 			this.$el.addClass( 'is-installed' );
       
   401 		}
   410 		}
   402 	},
   411 	},
   403 
   412 
   404 	// Adds a class to the currently active theme
   413 	// Adds a class to the currently active theme
   405 	// and to the overlay in detailed view mode
   414 	// and to the overlay in detailed view mode
   438 		// one of the direct action buttons
   447 		// one of the direct action buttons
   439 		if ( $( event.target ).is( '.theme-actions a' ) ) {
   448 		if ( $( event.target ).is( '.theme-actions a' ) ) {
   440 			return;
   449 			return;
   441 		}
   450 		}
   442 
   451 
       
   452 		// Prevent the modal from showing when the user clicks one of the direct action buttons.
       
   453 		if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
       
   454 			return;
       
   455 		}
       
   456 
   443 		// Set focused theme to current element
   457 		// Set focused theme to current element
   444 		themes.focusedTheme = this.$el;
   458 		themes.focusedTheme = this.$el;
   445 
   459 
   446 		this.trigger( 'theme:expand', self.model.cid );
   460 		this.trigger( 'theme:expand', self.model.cid );
   447 	},
   461 	},
   452 
   466 
   453 	preview: function( event ) {
   467 	preview: function( event ) {
   454 		var self = this,
   468 		var self = this,
   455 			current, preview;
   469 			current, preview;
   456 
   470 
       
   471 		event = event || window.event;
       
   472 
   457 		// Bail if the user scrolled on a touch device
   473 		// Bail if the user scrolled on a touch device
   458 		if ( this.touchDrag === true ) {
   474 		if ( this.touchDrag === true ) {
   459 			return this.touchDrag = false;
   475 			return this.touchDrag = false;
   460 		}
   476 		}
   461 
   477 
   462 		// Allow direct link path to installing a theme.
   478 		// Allow direct link path to installing a theme.
   463 		if ( $( event.target ).hasClass( 'button-primary' ) ) {
   479 		if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
   464 			return;
   480 			return;
   465 		}
   481 		}
   466 
   482 
   467 		// 'enter' and 'space' keys expand the details view when a theme is :focused
   483 		// 'enter' and 'space' keys expand the details view when a theme is :focused
   468 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
   484 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
   480 
   496 
   481 		// Set focus to current theme.
   497 		// Set focus to current theme.
   482 		themes.focusedTheme = this.$el;
   498 		themes.focusedTheme = this.$el;
   483 
   499 
   484 		// Construct a new Preview view.
   500 		// Construct a new Preview view.
   485 		preview = new themes.view.Preview({
   501 		themes.preview = preview = new themes.view.Preview({
   486 			model: this.model
   502 			model: this.model
   487 		});
   503 		});
   488 
   504 
   489 		// Render the view and append it.
   505 		// Render the view and append it.
   490 		preview.render();
   506 		preview.render();
   560 		});
   576 		});
   561 
   577 
   562 		this.listenTo( preview, 'preview:close', function() {
   578 		this.listenTo( preview, 'preview:close', function() {
   563 			self.current = self.model;
   579 			self.current = self.model;
   564 		});
   580 		});
       
   581 
   565 	},
   582 	},
   566 
   583 
   567 	// Handles .disabled classes for previous/next buttons in theme installer preview
   584 	// Handles .disabled classes for previous/next buttons in theme installer preview
   568 	setNavButtonsState: function() {
   585 	setNavButtonsState: function() {
   569 		var $themeInstaller = $( '.theme-install-overlay' ),
   586 		var $themeInstaller = $( '.theme-install-overlay' ),
   576 
   593 
   577 		// Disable next if the next model is undefined
   594 		// Disable next if the next model is undefined
   578 		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
   595 		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
   579 			$themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
   596 			$themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
   580 		}
   597 		}
       
   598 	},
       
   599 
       
   600 	installTheme: function( event ) {
       
   601 		var _this = this;
       
   602 
       
   603 		event.preventDefault();
       
   604 
       
   605 		wp.updates.maybeRequestFilesystemCredentials( event );
       
   606 
       
   607 		$( document ).on( 'wp-theme-install-success', function( event, response ) {
       
   608 			if ( _this.model.get( 'id' ) === response.slug ) {
       
   609 				_this.model.set( { 'installed': true } );
       
   610 			}
       
   611 		} );
       
   612 
       
   613 		wp.updates.installTheme( {
       
   614 			slug: $( event.target ).data( 'slug' )
       
   615 		} );
       
   616 	},
       
   617 
       
   618 	updateTheme: function( event ) {
       
   619 		var _this = this;
       
   620 
       
   621 		if ( ! this.model.get( 'hasPackage' ) ) {
       
   622 			return;
       
   623 		}
       
   624 
       
   625 		event.preventDefault();
       
   626 
       
   627 		wp.updates.maybeRequestFilesystemCredentials( event );
       
   628 
       
   629 		$( document ).on( 'wp-theme-update-success', function( event, response ) {
       
   630 			_this.model.off( 'change', _this.render, _this );
       
   631 			if ( _this.model.get( 'id' ) === response.slug ) {
       
   632 				_this.model.set( {
       
   633 					hasUpdate: false,
       
   634 					version: response.newVersion
       
   635 				} );
       
   636 			}
       
   637 			_this.model.on( 'change', _this.render, _this );
       
   638 		} );
       
   639 
       
   640 		wp.updates.updateTheme( {
       
   641 			slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
       
   642 		} );
   581 	}
   643 	}
   582 });
   644 });
   583 
   645 
   584 // Theme Details view
   646 // Theme Details view
   585 // Set ups a modal overlay with the expanded theme data
   647 // Set ups a modal overlay with the expanded theme data
   590 
   652 
   591 	events: {
   653 	events: {
   592 		'click': 'collapse',
   654 		'click': 'collapse',
   593 		'click .delete-theme': 'deleteTheme',
   655 		'click .delete-theme': 'deleteTheme',
   594 		'click .left': 'previousTheme',
   656 		'click .left': 'previousTheme',
   595 		'click .right': 'nextTheme'
   657 		'click .right': 'nextTheme',
       
   658 		'click #update-theme': 'updateTheme'
   596 	},
   659 	},
   597 
   660 
   598 	// The HTML template for the theme overlay
   661 	// The HTML template for the theme overlay
   599 	html: themes.template( 'theme-single' ),
   662 	html: themes.template( 'theme-single' ),
   600 
   663 
   616 	activeTheme: function() {
   679 	activeTheme: function() {
   617 		// Check the model has the active property
   680 		// Check the model has the active property
   618 		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
   681 		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
   619 	},
   682 	},
   620 
   683 
   621 	// Keeps :focus within the theme details elements
   684 	// Set initial focus and constrain tabbing within the theme browser modal.
   622 	containFocus: function( $el ) {
   685 	containFocus: function( $el ) {
   623 		var $target;
   686 
   624 
   687 		// Set initial focus on the primary action control.
   625 		// Move focus to the primary action
       
   626 		_.delay( function() {
   688 		_.delay( function() {
   627 			$( '.theme-wrap a.button-primary:visible' ).focus();
   689 			$( '.theme-overlay' ).focus();
   628 		}, 500 );
   690 		}, 100 );
   629 
   691 
       
   692 		// Constrain tabbing within the modal.
   630 		$el.on( 'keydown.wp-themes', function( event ) {
   693 		$el.on( 'keydown.wp-themes', function( event ) {
   631 
   694 			var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(),
   632 			// Tab key
   695 				$lastFocusable = $el.find( '.theme-actions a:visible' ).last();
   633 			if ( event.which === 9 ) {
   696 
   634 				$target = $( event.target );
   697 			// Check for the Tab key.
   635 
   698 			if ( 9 === event.which ) {
   636 				// Keep focus within the overlay by making the last link on theme actions
   699 				if ( $firstFocusable[0] === event.target && event.shiftKey ) {
   637 				// switch focus to button.left on tabbing and vice versa
   700 					$lastFocusable.focus();
   638 				if ( $target.is( 'button.left' ) && event.shiftKey ) {
       
   639 					$el.find( '.theme-actions a:last-child' ).focus();
       
   640 					event.preventDefault();
   701 					event.preventDefault();
   641 				} else if ( $target.is( '.theme-actions a:last-child' ) ) {
   702 				} else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
   642 					$el.find( 'button.left' ).focus();
   703 					$firstFocusable.focus();
   643 					event.preventDefault();
   704 					event.preventDefault();
   644 				}
   705 				}
   645 			}
   706 			}
   646 		});
   707 		});
   647 	},
   708 	},
   694 	// Handles .disabled classes for next/previous buttons
   755 	// Handles .disabled classes for next/previous buttons
   695 	navigation: function() {
   756 	navigation: function() {
   696 
   757 
   697 		// Disable Left/Right when at the start or end of the collection
   758 		// Disable Left/Right when at the start or end of the collection
   698 		if ( this.model.cid === this.model.collection.at(0).cid ) {
   759 		if ( this.model.cid === this.model.collection.at(0).cid ) {
   699 			this.$el.find( '.left' ).addClass( 'disabled' );
   760 			this.$el.find( '.left' )
       
   761 				.addClass( 'disabled' )
       
   762 				.prop( 'disabled', true );
   700 		}
   763 		}
   701 		if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
   764 		if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
   702 			this.$el.find( '.right' ).addClass( 'disabled' );
   765 			this.$el.find( '.right' )
       
   766 				.addClass( 'disabled' )
       
   767 				.prop( 'disabled', true );
   703 		}
   768 		}
   704 	},
   769 	},
   705 
   770 
   706 	// Performs the actions to effectively close
   771 	// Performs the actions to effectively close
   707 	// the theme details overlay
   772 	// the theme details overlay
   710 		this.remove();
   775 		this.remove();
   711 		this.unbind();
   776 		this.unbind();
   712 		this.trigger( 'theme:collapse' );
   777 		this.trigger( 'theme:collapse' );
   713 	},
   778 	},
   714 
   779 
   715 	// Confirmation dialog for deleting a theme
   780 	updateTheme: function( event ) {
   716 	deleteTheme: function() {
   781 		var _this = this;
   717 		return confirm( themes.data.settings.confirmDelete );
   782 		event.preventDefault();
       
   783 
       
   784 		wp.updates.maybeRequestFilesystemCredentials( event );
       
   785 
       
   786 		$( document ).on( 'wp-theme-update-success', function( event, response ) {
       
   787 			if ( _this.model.get( 'id' ) === response.slug ) {
       
   788 				_this.model.set( {
       
   789 					hasUpdate: false,
       
   790 					version: response.newVersion
       
   791 				} );
       
   792 			}
       
   793 			_this.render();
       
   794 		} );
       
   795 
       
   796 		wp.updates.updateTheme( {
       
   797 			slug: $( event.target ).data( 'slug' )
       
   798 		} );
       
   799 	},
       
   800 
       
   801 	deleteTheme: function( event ) {
       
   802 		var _this = this,
       
   803 		    _collection = _this.model.collection,
       
   804 		    _themes = themes;
       
   805 		event.preventDefault();
       
   806 
       
   807 		// Confirmation dialog for deleting a theme.
       
   808 		if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
       
   809 			return;
       
   810 		}
       
   811 
       
   812 		wp.updates.maybeRequestFilesystemCredentials( event );
       
   813 
       
   814 		$( document ).one( 'wp-theme-delete-success', function( event, response ) {
       
   815 			_this.$el.find( '.close' ).trigger( 'click' );
       
   816 			$( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
       
   817 				$( this ).remove();
       
   818 				_themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
       
   819 
       
   820 				$( '.wp-filter-search' ).val( '' );
       
   821 				_collection.doSearch( '' );
       
   822 				_collection.remove( _this.model );
       
   823 				_collection.trigger( 'themes:update' );
       
   824 			} );
       
   825 		} );
       
   826 
       
   827 		wp.updates.deleteTheme( {
       
   828 			slug: this.model.get( 'id' )
       
   829 		} );
   718 	},
   830 	},
   719 
   831 
   720 	nextTheme: function() {
   832 	nextTheme: function() {
   721 		var self = this;
   833 		var self = this;
   722 		self.trigger( 'theme:next', self.model.cid );
   834 		self.trigger( 'theme:next', self.model.cid );
   753 	el: '.theme-install-overlay',
   865 	el: '.theme-install-overlay',
   754 
   866 
   755 	events: {
   867 	events: {
   756 		'click .close-full-overlay': 'close',
   868 		'click .close-full-overlay': 'close',
   757 		'click .collapse-sidebar': 'collapse',
   869 		'click .collapse-sidebar': 'collapse',
       
   870 		'click .devices button': 'previewDevice',
   758 		'click .previous-theme': 'previousTheme',
   871 		'click .previous-theme': 'previousTheme',
   759 		'click .next-theme': 'nextTheme',
   872 		'click .next-theme': 'nextTheme',
   760 		'keyup': 'keyEvent'
   873 		'keyup': 'keyEvent',
       
   874 		'click .theme-install': 'installTheme'
   761 	},
   875 	},
   762 
   876 
   763 	// The HTML template for the theme preview
   877 	// The HTML template for the theme preview
   764 	html: themes.template( 'theme-preview' ),
   878 	html: themes.template( 'theme-preview' ),
   765 
   879 
   766 	render: function() {
   880 	render: function() {
   767 		var data = this.model.toJSON();
   881 		var self = this,
   768 
   882 			currentPreviewDevice,
   769 		this.$el.html( this.html( data ) );
   883 			data = this.model.toJSON(),
   770 
   884 			$body = $( document.body );
   771 		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: true } );
   885 
       
   886 		$body.attr( 'aria-busy', 'true' );
       
   887 
       
   888 		this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
       
   889 
       
   890 		currentPreviewDevice = this.$el.data( 'current-preview-device' );
       
   891 		if ( currentPreviewDevice ) {
       
   892 			self.tooglePreviewDeviceButtons( currentPreviewDevice );
       
   893 		}
       
   894 
       
   895 		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } );
   772 
   896 
   773 		this.$el.fadeIn( 200, function() {
   897 		this.$el.fadeIn( 200, function() {
   774 			$( 'body' ).addClass( 'theme-installer-active full-overlay-active' );
   898 			$body.addClass( 'theme-installer-active full-overlay-active' );
   775 			$( '.close-full-overlay' ).focus();
   899 		});
   776 		});
   900 
       
   901 		this.$el.find( 'iframe' ).one( 'load', function() {
       
   902 			self.iframeLoaded();
       
   903 		});
       
   904 	},
       
   905 
       
   906 	iframeLoaded: function() {
       
   907 		this.$el.addClass( 'iframe-ready' );
       
   908 		$( document.body ).attr( 'aria-busy', 'false' );
   777 	},
   909 	},
   778 
   910 
   779 	close: function() {
   911 	close: function() {
   780 		this.$el.fadeOut( 200, function() {
   912 		this.$el.fadeOut( 200, function() {
   781 			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
   913 			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
   782 
   914 
   783 			// Return focus to the theme div
   915 			// Return focus to the theme div
   784 			if ( themes.focusedTheme ) {
   916 			if ( themes.focusedTheme ) {
   785 				themes.focusedTheme.focus();
   917 				themes.focusedTheme.focus();
   786 			}
   918 			}
   787 		});
   919 		}).removeClass( 'iframe-ready' );
   788 
   920 
   789 		themes.router.navigate( themes.router.baseUrl( '' ) );
   921 		// Restore the previous browse tab if available.
       
   922 		if ( themes.router.selectedTab ) {
       
   923 			themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) );
       
   924 			themes.router.selectedTab = false;
       
   925 		} else {
       
   926 			themes.router.navigate( themes.router.baseUrl( '' ) );
       
   927 		}
   790 		this.trigger( 'preview:close' );
   928 		this.trigger( 'preview:close' );
   791 		this.undelegateEvents();
   929 		this.undelegateEvents();
   792 		this.unbind();
   930 		this.unbind();
   793 		return false;
   931 		return false;
   794 	},
   932 	},
   795 
   933 
   796 	collapse: function() {
   934 	collapse: function( event ) {
       
   935 		var $button = $( event.currentTarget );
       
   936 		if ( 'true' === $button.attr( 'aria-expanded' ) ) {
       
   937 			$button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
       
   938 		} else {
       
   939 			$button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
       
   940 		}
   797 
   941 
   798 		this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
   942 		this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
   799 		return false;
   943 		return false;
       
   944 	},
       
   945 
       
   946 	previewDevice: function( event ) {
       
   947 		var device = $( event.currentTarget ).data( 'device' );
       
   948 
       
   949 		this.$el
       
   950 			.removeClass( 'preview-desktop preview-tablet preview-mobile' )
       
   951 			.addClass( 'preview-' + device )
       
   952 			.data( 'current-preview-device', device );
       
   953 
       
   954 		this.tooglePreviewDeviceButtons( device );
       
   955 	},
       
   956 
       
   957 	tooglePreviewDeviceButtons: function( newDevice ) {
       
   958 		var $devices = $( '.wp-full-overlay-footer .devices' );
       
   959 
       
   960 		$devices.find( 'button' )
       
   961 			.removeClass( 'active' )
       
   962 			.attr( 'aria-pressed', false );
       
   963 
       
   964 		$devices.find( 'button.preview-' + newDevice )
       
   965 			.addClass( 'active' )
       
   966 			.attr( 'aria-pressed', true );
   800 	},
   967 	},
   801 
   968 
   802 	keyEvent: function( event ) {
   969 	keyEvent: function( event ) {
   803 		// The escape key closes the preview
   970 		// The escape key closes the preview
   804 		if ( event.keyCode === 27 ) {
   971 		if ( event.keyCode === 27 ) {
   812 
   979 
   813 		// The left arrow key, previous theme
   980 		// The left arrow key, previous theme
   814 		if ( event.keyCode === 37 ) {
   981 		if ( event.keyCode === 37 ) {
   815 			this.previousTheme();
   982 			this.previousTheme();
   816 		}
   983 		}
       
   984 	},
       
   985 
       
   986 	installTheme: function( event ) {
       
   987 		var _this   = this,
       
   988 		    $target = $( event.target );
       
   989 		event.preventDefault();
       
   990 
       
   991 		if ( $target.hasClass( 'disabled' ) ) {
       
   992 			return;
       
   993 		}
       
   994 
       
   995 		wp.updates.maybeRequestFilesystemCredentials( event );
       
   996 
       
   997 		$( document ).on( 'wp-theme-install-success', function() {
       
   998 			_this.model.set( { 'installed': true } );
       
   999 		} );
       
  1000 
       
  1001 		wp.updates.installTheme( {
       
  1002 			slug: $target.data( 'slug' )
       
  1003 		} );
   817 	}
  1004 	}
   818 });
  1005 });
   819 
  1006 
   820 // Controls the rendering of div.themes,
  1007 // Controls the rendering of div.themes,
   821 // a wrapper that will hold all the theme elements
  1008 // a wrapper that will hold all the theme elements
   822 themes.view.Themes = wp.Backbone.View.extend({
  1009 themes.view.Themes = wp.Backbone.View.extend({
   823 
  1010 
   824 	className: 'themes',
  1011 	className: 'themes wp-clearfix',
   825 	$overlay: $( 'div.theme-overlay' ),
  1012 	$overlay: $( 'div.theme-overlay' ),
   826 
  1013 
   827 	// Number to keep track of scroll position
  1014 	// Number to keep track of scroll position
   828 	// while in theme-overlay mode
  1015 	// while in theme-overlay mode
   829 	index: 0,
  1016 	index: 0,
   830 
  1017 
   831 	// The theme count element
  1018 	// The theme count element
   832 	count: $( '.wp-core-ui .theme-count' ),
  1019 	count: $( '.wrap .theme-count' ),
   833 
  1020 
   834 	// The live themes count
  1021 	// The live themes count
   835 	liveThemeCount: 0,
  1022 	liveThemeCount: 0,
   836 
  1023 
   837 	initialize: function( options ) {
  1024 	initialize: function( options ) {
   845 
  1032 
   846 		// Move the active theme to the beginning of the collection
  1033 		// Move the active theme to the beginning of the collection
   847 		self.currentTheme();
  1034 		self.currentTheme();
   848 
  1035 
   849 		// When the collection is updated by user input...
  1036 		// When the collection is updated by user input...
   850 		this.listenTo( self.collection, 'update', function() {
  1037 		this.listenTo( self.collection, 'themes:update', function() {
   851 			self.parent.page = 0;
  1038 			self.parent.page = 0;
   852 			self.currentTheme();
  1039 			self.currentTheme();
   853 			self.render( this );
  1040 			self.render( this );
   854 		});
  1041 		} );
   855 
  1042 
   856 		// Update theme count to full result set when available.
  1043 		// Update theme count to full result set when available.
   857 		this.listenTo( self.collection, 'query:success', function( count ) {
  1044 		this.listenTo( self.collection, 'query:success', function( count ) {
   858 			if ( _.isNumber( count ) ) {
  1045 			if ( _.isNumber( count ) ) {
   859 				self.count.text( count );
  1046 				self.count.text( count );
   882 		$( 'body' ).on( 'keyup', function( event ) {
  1069 		$( 'body' ).on( 'keyup', function( event ) {
   883 			if ( ! self.overlay ) {
  1070 			if ( ! self.overlay ) {
   884 				return;
  1071 				return;
   885 			}
  1072 			}
   886 
  1073 
       
  1074 			// Bail if the filesystem credentials dialog is shown.
       
  1075 			if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
       
  1076 				return;
       
  1077 			}
       
  1078 
   887 			// Pressing the right arrow key fires a theme:next event
  1079 			// Pressing the right arrow key fires a theme:next event
   888 			if ( event.keyCode === 39 ) {
  1080 			if ( event.keyCode === 39 ) {
   889 				self.overlay.nextTheme();
  1081 				self.overlay.nextTheme();
   890 			}
  1082 			}
   891 
  1083 
   932 
  1124 
   933 		// Display a live theme count for the collection
  1125 		// Display a live theme count for the collection
   934 		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
  1126 		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
   935 		this.count.text( this.liveThemeCount );
  1127 		this.count.text( this.liveThemeCount );
   936 
  1128 
   937 		this.announceSearchResults( this.liveThemeCount );
  1129 		/*
       
  1130 		 * In the theme installer the themes count is already announced
       
  1131 		 * because `announceSearchResults` is called on `query:success`.
       
  1132 		 */
       
  1133 		if ( ! themes.isInstall ) {
       
  1134 			this.announceSearchResults( this.liveThemeCount );
       
  1135 		}
   938 	},
  1136 	},
   939 
  1137 
   940 	// Iterates through each instance of the collection
  1138 	// Iterates through each instance of the collection
   941 	// and renders each theme module
  1139 	// and renders each theme module
   942 	renderThemes: function( page ) {
  1140 	renderThemes: function( page ) {
   950 			this.parent.trigger( 'theme:end' );
  1148 			this.parent.trigger( 'theme:end' );
   951 			return;
  1149 			return;
   952 		}
  1150 		}
   953 
  1151 
   954 		// Make sure the add-new stays at the end
  1152 		// Make sure the add-new stays at the end
   955 		if ( page >= 1 ) {
  1153 		if ( ! themes.isInstall && page >= 1 ) {
   956 			$( '.add-new-theme' ).remove();
  1154 			$( '.add-new-theme' ).remove();
   957 		}
  1155 		}
   958 
  1156 
   959 		// Loop through the themes and setup each theme view
  1157 		// Loop through the themes and setup each theme view
   960 		self.instance.each( function( theme ) {
  1158 		self.instance.each( function( theme ) {
   972 			// with the theme details
  1170 			// with the theme details
   973 			self.listenTo( self.theme, 'theme:expand', self.expand, self );
  1171 			self.listenTo( self.theme, 'theme:expand', self.expand, self );
   974 		});
  1172 		});
   975 
  1173 
   976 		// 'Add new theme' element shown at the end of the grid
  1174 		// 'Add new theme' element shown at the end of the grid
   977 		if ( themes.data.settings.canInstall ) {
  1175 		if ( ! themes.isInstall && themes.data.settings.canInstall ) {
   978 			this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h3 class="theme-name">' + l10n.addNew + '</h3></a></div>' );
  1176 			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>' );
   979 		}
  1177 		}
   980 
  1178 
   981 		this.parent.page++;
  1179 		this.parent.page++;
   982 	},
  1180 	},
   983 
  1181 
  1001 	},
  1199 	},
  1002 
  1200 
  1003 	// Renders the overlay with the ThemeDetails view
  1201 	// Renders the overlay with the ThemeDetails view
  1004 	// Uses the current model data
  1202 	// Uses the current model data
  1005 	expand: function( id ) {
  1203 	expand: function( id ) {
  1006 		var self = this;
  1204 		var self = this, $card, $modal;
  1007 
  1205 
  1008 		// Set the current theme model
  1206 		// Set the current theme model
  1009 		this.model = self.collection.get( id );
  1207 		this.model = self.collection.get( id );
  1010 
  1208 
  1011 		// Trigger a route update for the current model
  1209 		// Trigger a route update for the current model
  1019 		this.overlay = new themes.view.Details({
  1217 		this.overlay = new themes.view.Details({
  1020 			model: self.model
  1218 			model: self.model
  1021 		});
  1219 		});
  1022 
  1220 
  1023 		this.overlay.render();
  1221 		this.overlay.render();
       
  1222 
       
  1223 		if ( this.model.get( 'hasUpdate' ) ) {
       
  1224 			$card  = $( '[data-slug="' + this.model.id + '"]' );
       
  1225 			$modal = $( this.overlay.el );
       
  1226 
       
  1227 			if ( $card.find( '.updating-message' ).length ) {
       
  1228 				$modal.find( '.notice-warning h3' ).remove();
       
  1229 				$modal.find( '.notice-warning' )
       
  1230 					.removeClass( 'notice-large' )
       
  1231 					.addClass( 'updating-message' )
       
  1232 					.find( 'p' ).text( wp.updates.l10n.updating );
       
  1233 			} else if ( $card.find( '.notice-error' ).length ) {
       
  1234 				$modal.find( '.notice-warning' ).remove();
       
  1235 			}
       
  1236 		}
       
  1237 
  1024 		this.$overlay.html( this.overlay.el );
  1238 		this.$overlay.html( this.overlay.el );
  1025 
  1239 
  1026 		// Bind to theme:next and theme:previous
  1240 		// Bind to theme:next and theme:previous
  1027 		// triggered by the arrow keys
  1241 		// triggered by the arrow keys
  1028 		//
  1242 		//
  1132 		// Clear on escape.
  1346 		// Clear on escape.
  1133 		if ( event.type === 'keyup' && event.which === 27 ) {
  1347 		if ( event.type === 'keyup' && event.which === 27 ) {
  1134 			event.target.value = '';
  1348 			event.target.value = '';
  1135 		}
  1349 		}
  1136 
  1350 
  1137 		/**
  1351 		// Since doSearch is debounced, it will only run when user input comes to a rest.
  1138 		 * Since doSearch is debounced, it will only run when user input comes to a rest
       
  1139 		 */
       
  1140 		this.doSearch( event );
  1352 		this.doSearch( event );
  1141 	},
  1353 	},
  1142 
  1354 
  1143 	// Runs a search on the theme collection.
  1355 	// Runs a search on the theme collection.
  1144 	doSearch: _.debounce( function( event ) {
  1356 	doSearch: function( event ) {
  1145 		var options = {};
  1357 		var options = {};
  1146 
  1358 
  1147 		this.collection.doSearch( event.target.value );
  1359 		this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
  1148 
  1360 
  1149 		// if search is initiated and key is not return
  1361 		// if search is initiated and key is not return
  1150 		if ( this.searching && event.which !== 13 ) {
  1362 		if ( this.searching && event.which !== 13 ) {
  1151 			options.replace = true;
  1363 			options.replace = true;
  1152 		} else {
  1364 		} else {
  1157 		if ( event.target.value ) {
  1369 		if ( event.target.value ) {
  1158 			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
  1370 			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
  1159 		} else {
  1371 		} else {
  1160 			themes.router.navigate( themes.router.baseUrl( '' ) );
  1372 			themes.router.navigate( themes.router.baseUrl( '' ) );
  1161 		}
  1373 		}
  1162 	}, 500 ),
  1374 	},
  1163 
  1375 
  1164 	pushState: function( event ) {
  1376 	pushState: function( event ) {
  1165 		var url = themes.router.baseUrl( '' );
  1377 		var url = themes.router.baseUrl( '' );
  1166 
  1378 
  1167 		if ( event.target.value ) {
  1379 		if ( event.target.value ) {
  1168 			url = themes.router.baseUrl( themes.router.searchPath + event.target.value );
  1380 			url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) );
  1169 		}
  1381 		}
  1170 
  1382 
  1171 		this.searching = false;
  1383 		this.searching = false;
  1172 		themes.router.navigate( url );
  1384 		themes.router.navigate( url );
  1173 
  1385 
  1174 	}
  1386 	}
  1175 });
  1387 });
       
  1388 
       
  1389 /**
       
  1390  * Navigate router.
       
  1391  *
       
  1392  * @since 4.9.0
       
  1393  *
       
  1394  * @param {string} url - URL to navigate to.
       
  1395  * @param {object} state - State.
       
  1396  * @returns {void}
       
  1397  */
       
  1398 function navigateRouter( url, state ) {
       
  1399 	var router = this;
       
  1400 	if ( Backbone.history._hasPushState ) {
       
  1401 		Backbone.Router.prototype.navigate.call( router, url, state );
       
  1402 	}
       
  1403 }
  1176 
  1404 
  1177 // Sets up the routes events for relevant url queries
  1405 // Sets up the routes events for relevant url queries
  1178 // Listens to [theme] and [search] params
  1406 // Listens to [theme] and [search] params
  1179 themes.Router = Backbone.Router.extend({
  1407 themes.Router = Backbone.Router.extend({
  1180 
  1408 
  1192 
  1420 
  1193 	themePath: '?theme=',
  1421 	themePath: '?theme=',
  1194 	searchPath: '?search=',
  1422 	searchPath: '?search=',
  1195 
  1423 
  1196 	search: function( query ) {
  1424 	search: function( query ) {
  1197 		$( '.wp-filter-search' ).val( query );
  1425 		$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
  1198 	},
  1426 	},
  1199 
  1427 
  1200 	themes: function() {
  1428 	themes: function() {
  1201 		$( '.wp-filter-search' ).val( '' );
  1429 		$( '.wp-filter-search' ).val( '' );
  1202 	},
  1430 	},
  1203 
  1431 
  1204 	navigate: function() {
  1432 	navigate: navigateRouter
  1205 		if ( Backbone.history._hasPushState ) {
       
  1206 			Backbone.Router.prototype.navigate.apply( this, arguments );
       
  1207 		}
       
  1208 	}
       
  1209 
  1433 
  1210 });
  1434 });
  1211 
  1435 
  1212 // Execute and setup the application
  1436 // Execute and setup the application
  1213 themes.Run = {
  1437 themes.Run = {
  1220 		this.view = new themes.view.Appearance({
  1444 		this.view = new themes.view.Appearance({
  1221 			collection: this.themes
  1445 			collection: this.themes
  1222 		});
  1446 		});
  1223 
  1447 
  1224 		this.render();
  1448 		this.render();
       
  1449 
       
  1450 		// Start debouncing user searches after Backbone.history.start().
       
  1451 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
  1225 	},
  1452 	},
  1226 
  1453 
  1227 	render: function() {
  1454 	render: function() {
  1228 
  1455 
  1229 		// Render results
  1456 		// Render results
  1230 		this.view.render();
  1457 		this.view.render();
  1231 		this.routes();
  1458 		this.routes();
  1232 
  1459 
       
  1460 		if ( Backbone.History.started ) {
       
  1461 			Backbone.history.stop();
       
  1462 		}
  1233 		Backbone.history.start({
  1463 		Backbone.history.start({
  1234 			root: themes.data.settings.adminUrl,
  1464 			root: themes.data.settings.adminUrl,
  1235 			pushState: true,
  1465 			pushState: true,
  1236 			hashChange: false
  1466 			hashChange: false
  1237 		});
  1467 		});
  1272 	events: {
  1502 	events: {
  1273 		'input': 'search',
  1503 		'input': 'search',
  1274 		'keyup': 'search'
  1504 		'keyup': 'search'
  1275 	},
  1505 	},
  1276 
  1506 
       
  1507 	terms: '',
       
  1508 
  1277 	// Handles Ajax request for searching through themes in public repo
  1509 	// Handles Ajax request for searching through themes in public repo
  1278 	search: function( event ) {
  1510 	search: function( event ) {
  1279 
  1511 
  1280 		// Tabbing or reverse tabbing into the search input shouldn't trigger a search
  1512 		// Tabbing or reverse tabbing into the search input shouldn't trigger a search
  1281 		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
  1513 		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
  1290 		}
  1522 		}
  1291 
  1523 
  1292 		this.doSearch( event.target.value );
  1524 		this.doSearch( event.target.value );
  1293 	},
  1525 	},
  1294 
  1526 
  1295 	doSearch: _.debounce( function( value ) {
  1527 	doSearch: function( value ) {
  1296 		var request = {};
  1528 		var request = {};
       
  1529 
       
  1530 		// Don't do anything if the search terms haven't changed.
       
  1531 		if ( this.terms === value ) {
       
  1532 			return;
       
  1533 		}
       
  1534 
       
  1535 		// Updates terms with the value passed.
       
  1536 		this.terms = value;
  1297 
  1537 
  1298 		request.search = value;
  1538 		request.search = value;
  1299 
  1539 
  1300 		// Intercept an [author] search.
  1540 		// Intercept an [author] search.
  1301 		//
  1541 		//
  1313 		if ( value.substring( 0, 4 ) === 'tag:' ) {
  1553 		if ( value.substring( 0, 4 ) === 'tag:' ) {
  1314 			request.search = '';
  1554 			request.search = '';
  1315 			request.tag = [ value.slice( 4 ) ];
  1555 			request.tag = [ value.slice( 4 ) ];
  1316 		}
  1556 		}
  1317 
  1557 
  1318 		$( '.filter-links li > a.current' ).removeClass( 'current' );
  1558 		$( '.filter-links li > a.current' )
  1319 		$( 'body' ).removeClass( 'show-filters filters-applied' );
  1559 			.removeClass( 'current' )
       
  1560 			.removeAttr( 'aria-current' );
       
  1561 
       
  1562 		$( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
       
  1563 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1320 
  1564 
  1321 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1565 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1322 		// or searching the local cache
  1566 		// or searching the local cache
  1323 		this.collection.query( request );
  1567 		this.collection.query( request );
  1324 
  1568 
  1325 		// Set route
  1569 		// Set route
  1326 		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + value ), { replace: true } );
  1570 		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
  1327 	}, 500 )
  1571 	}
  1328 });
  1572 });
  1329 
  1573 
  1330 themes.view.Installer = themes.view.Appearance.extend({
  1574 themes.view.Installer = themes.view.Appearance.extend({
  1331 
  1575 
  1332 	el: '#wpbody-content .wrap',
  1576 	el: '#wpbody-content .wrap',
  1337 		'click .theme-filter': 'onFilter',
  1581 		'click .theme-filter': 'onFilter',
  1338 		'click .drawer-toggle': 'moreFilters',
  1582 		'click .drawer-toggle': 'moreFilters',
  1339 		'click .filter-drawer .apply-filters': 'applyFilters',
  1583 		'click .filter-drawer .apply-filters': 'applyFilters',
  1340 		'click .filter-group [type="checkbox"]': 'addFilter',
  1584 		'click .filter-group [type="checkbox"]': 'addFilter',
  1341 		'click .filter-drawer .clear-filters': 'clearFilters',
  1585 		'click .filter-drawer .clear-filters': 'clearFilters',
  1342 		'click .filtered-by': 'backToFilters'
  1586 		'click .edit-filters': 'backToFilters',
       
  1587 		'click .favorites-form-submit' : 'saveUsername',
       
  1588 		'keyup #wporg-username-input': 'saveUsername'
  1343 	},
  1589 	},
  1344 
  1590 
  1345 	// Initial render method
  1591 	// Initial render method
  1346 	render: function() {
  1592 	render: function() {
  1347 		var self = this;
  1593 		var self = this;
  1374 		});
  1620 		});
  1375 
  1621 
  1376 		this.listenTo( this.collection, 'query:fail', function() {
  1622 		this.listenTo( this.collection, 'query:fail', function() {
  1377 			$( 'body' ).removeClass( 'loading-content' );
  1623 			$( 'body' ).removeClass( 'loading-content' );
  1378 			$( '.theme-browser' ).find( 'div.error' ).remove();
  1624 			$( '.theme-browser' ).find( 'div.error' ).remove();
  1379 			$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p></div>' );
  1625 			$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
       
  1626 			$( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
       
  1627 				e.preventDefault();
       
  1628 				$( 'input.wp-filter-search' ).trigger( 'input' );
       
  1629 			} );
  1380 		});
  1630 		});
  1381 
  1631 
  1382 		if ( this.view ) {
  1632 		if ( this.view ) {
  1383 			this.view.remove();
  1633 			this.view.remove();
  1384 		}
  1634 		}
  1411 			sort = $el.data( 'sort' );
  1661 			sort = $el.data( 'sort' );
  1412 
  1662 
  1413 		event.preventDefault();
  1663 		event.preventDefault();
  1414 
  1664 
  1415 		$( 'body' ).removeClass( 'filters-applied show-filters' );
  1665 		$( 'body' ).removeClass( 'filters-applied show-filters' );
       
  1666 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1416 
  1667 
  1417 		// Bail if this is already active
  1668 		// Bail if this is already active
  1418 		if ( $el.hasClass( this.activeClass ) ) {
  1669 		if ( $el.hasClass( this.activeClass ) ) {
  1419 			return;
  1670 			return;
  1420 		}
  1671 		}
  1426 	},
  1677 	},
  1427 
  1678 
  1428 	sort: function( sort ) {
  1679 	sort: function( sort ) {
  1429 		this.clearSearch();
  1680 		this.clearSearch();
  1430 
  1681 
  1431 		$( '.filter-links li > a, .theme-filter' ).removeClass( this.activeClass );
  1682 		// Track sorting so we can restore the correct tab when closing preview.
  1432 		$( '[data-sort="' + sort + '"]' ).addClass( this.activeClass );
  1683 		themes.router.selectedTab = sort;
       
  1684 
       
  1685 		$( '.filter-links li > a, .theme-filter' )
       
  1686 			.removeClass( this.activeClass )
       
  1687 			.removeAttr( 'aria-current' );
       
  1688 
       
  1689 		$( '[data-sort="' + sort + '"]' )
       
  1690 			.addClass( this.activeClass )
       
  1691 			.attr( 'aria-current', 'page' );
       
  1692 
       
  1693 		if ( 'favorites' === sort ) {
       
  1694 			$( 'body' ).addClass( 'show-favorites-form' );
       
  1695 		} else {
       
  1696 			$( 'body' ).removeClass( 'show-favorites-form' );
       
  1697 		}
  1433 
  1698 
  1434 		this.browse( sort );
  1699 		this.browse( sort );
  1435 	},
  1700 	},
  1436 
  1701 
  1437 	// Filters and Tags
  1702 	// Filters and Tags
  1443 		// Bail if this is already active
  1708 		// Bail if this is already active
  1444 		if ( $el.hasClass( this.activeClass ) ) {
  1709 		if ( $el.hasClass( this.activeClass ) ) {
  1445 			return;
  1710 			return;
  1446 		}
  1711 		}
  1447 
  1712 
  1448 		$( '.filter-links li > a, .theme-section' ).removeClass( this.activeClass );
  1713 		$( '.filter-links li > a, .theme-section' )
  1449 		$el.addClass( this.activeClass );
  1714 			.removeClass( this.activeClass )
       
  1715 			.removeAttr( 'aria-current' );
       
  1716 		$el
       
  1717 			.addClass( this.activeClass )
       
  1718 			.attr( 'aria-current', 'page' );
  1450 
  1719 
  1451 		if ( ! filter ) {
  1720 		if ( ! filter ) {
  1452 			return;
  1721 			return;
  1453 		}
  1722 		}
  1454 
  1723 
  1455 		// Construct the filter request
  1724 		// Construct the filter request
  1456 		// using the default values
  1725 		// using the default values
  1457 		filter = _.union( filter, this.filtersChecked() );
  1726 		filter = _.union( [ filter, this.filtersChecked() ] );
  1458 		request = { tag: [ filter ] };
  1727 		request = { tag: [ filter ] };
  1459 
  1728 
  1460 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1729 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1461 		// or searching the local cache
  1730 		// or searching the local cache
  1462 		this.collection.query( request );
  1731 		this.collection.query( request );
  1476 
  1745 
  1477 		if ( event ) {
  1746 		if ( event ) {
  1478 			event.preventDefault();
  1747 			event.preventDefault();
  1479 		}
  1748 		}
  1480 
  1749 
       
  1750 		if ( ! tags ) {
       
  1751 			wp.a11y.speak( l10n.selectFeatureFilter );
       
  1752 			return;
       
  1753 		}
       
  1754 
  1481 		$( 'body' ).addClass( 'filters-applied' );
  1755 		$( 'body' ).addClass( 'filters-applied' );
  1482 		$( '.filter-links li > a.current' ).removeClass( 'current' );
  1756 		$( '.filter-links li > a.current' )
       
  1757 			.removeClass( 'current' )
       
  1758 			.removeAttr( 'aria-current' );
       
  1759 
  1483 		filteringBy.empty();
  1760 		filteringBy.empty();
  1484 
  1761 
  1485 		_.each( tags, function( tag ) {
  1762 		_.each( tags, function( tag ) {
  1486 			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
  1763 			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
  1487 			filteringBy.append( '<span class="tag">' + name + '</span>' );
  1764 			filteringBy.append( '<span class="tag">' + name + '</span>' );
  1488 		});
  1765 		});
  1489 
  1766 
  1490 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1767 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1491 		// or searching the local cache
  1768 		// or searching the local cache
  1492 		this.collection.query( request );
  1769 		this.collection.query( request );
       
  1770 	},
       
  1771 
       
  1772 	// Save the user's WordPress.org username and get his favorite themes.
       
  1773 	saveUsername: function ( event ) {
       
  1774 		var username = $( '#wporg-username-input' ).val(),
       
  1775 			nonce = $( '#wporg-username-nonce' ).val(),
       
  1776 			request = { browse: 'favorites', user: username },
       
  1777 			that = this;
       
  1778 
       
  1779 		if ( event ) {
       
  1780 			event.preventDefault();
       
  1781 		}
       
  1782 
       
  1783 		// save username on enter
       
  1784 		if ( event.type === 'keyup' && event.which !== 13 ) {
       
  1785 			return;
       
  1786 		}
       
  1787 
       
  1788 		return wp.ajax.send( 'save-wporg-username', {
       
  1789 			data: {
       
  1790 				_wpnonce: nonce,
       
  1791 				username: username
       
  1792 			},
       
  1793 			success: function () {
       
  1794 				// Get the themes by sending Ajax POST request to api.wordpress.org/themes
       
  1795 				// or searching the local cache
       
  1796 				that.collection.query( request );
       
  1797 			}
       
  1798 		} );
  1493 	},
  1799 	},
  1494 
  1800 
  1495 	// Get the checked filters
  1801 	// Get the checked filters
  1496 	// @return {array} of tags or false
  1802 	// @return {array} of tags or false
  1497 	filtersChecked: function() {
  1803 	filtersChecked: function() {
  1516 		return tags;
  1822 		return tags;
  1517 	},
  1823 	},
  1518 
  1824 
  1519 	activeClass: 'current',
  1825 	activeClass: 'current',
  1520 
  1826 
  1521 	// Overwrite search container class to append search
  1827 	/*
  1522 	// in new location
  1828 	 * When users press the "Upload Theme" button, show the upload form in place.
  1523 	searchContainer: $( '.wp-filter .search-form' ),
  1829 	 */
  1524 
       
  1525 	uploader: function() {
  1830 	uploader: function() {
  1526 		$( 'a.upload' ).on( 'click', function( event ) {
  1831 		var uploadViewToggle = $( '.upload-view-toggle' ),
  1527 			event.preventDefault();
  1832 			$body = $( document.body );
  1528 			$( 'body' ).addClass( 'show-upload-theme' );
  1833 
  1529 			themes.router.navigate( themes.router.baseUrl( '?upload' ), { replace: true } );
  1834 		uploadViewToggle.on( 'click', function() {
  1530 		});
  1835 			// Toggle the upload view.
  1531 		$( 'a.browse-themes' ).on( 'click', function( event ) {
  1836 			$body.toggleClass( 'show-upload-view' );
  1532 			event.preventDefault();
  1837 			// Toggle the `aria-expanded` button attribute.
  1533 			$( 'body' ).removeClass( 'show-upload-theme' );
  1838 			uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
  1534 			themes.router.navigate( themes.router.baseUrl( '' ), { replace: true } );
       
  1535 		});
  1839 		});
  1536 	},
  1840 	},
  1537 
  1841 
  1538 	// Toggle the full filters navigation
  1842 	// Toggle the full filters navigation
  1539 	moreFilters: function( event ) {
  1843 	moreFilters: function( event ) {
       
  1844 		var $body = $( 'body' ),
       
  1845 			$toggleButton = $( '.drawer-toggle' );
       
  1846 
  1540 		event.preventDefault();
  1847 		event.preventDefault();
  1541 
  1848 
  1542 		if ( $( 'body' ).hasClass( 'filters-applied' ) ) {
  1849 		if ( $body.hasClass( 'filters-applied' ) ) {
  1543 			return this.backToFilters();
  1850 			return this.backToFilters();
  1544 		}
  1851 		}
  1545 
  1852 
  1546 		// If the filters section is opened and filters are checked
       
  1547 		// run the relevant query collapsing to filtered-by state
       
  1548 		if ( $( 'body' ).hasClass( 'show-filters' ) && this.filtersChecked() ) {
       
  1549 			return this.addFilter();
       
  1550 		}
       
  1551 
       
  1552 		this.clearSearch();
  1853 		this.clearSearch();
  1553 
  1854 
  1554 		themes.router.navigate( themes.router.baseUrl( '' ) );
  1855 		themes.router.navigate( themes.router.baseUrl( '' ) );
  1555 		$( 'body' ).toggleClass( 'show-filters' );
  1856 		// Toggle the feature filters view.
       
  1857 		$body.toggleClass( 'show-filters' );
       
  1858 		// Toggle the `aria-expanded` button attribute.
       
  1859 		$toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
  1556 	},
  1860 	},
  1557 
  1861 
  1558 	// Clears all the checked filters
  1862 	// Clears all the checked filters
  1559 	// @uses filtersChecked()
  1863 	// @uses filtersChecked()
  1560 	clearFilters: function( event ) {
  1864 	clearFilters: function( event ) {
  1584 
  1888 
  1585 themes.InstallerRouter = Backbone.Router.extend({
  1889 themes.InstallerRouter = Backbone.Router.extend({
  1586 	routes: {
  1890 	routes: {
  1587 		'theme-install.php?theme=:slug': 'preview',
  1891 		'theme-install.php?theme=:slug': 'preview',
  1588 		'theme-install.php?browse=:sort': 'sort',
  1892 		'theme-install.php?browse=:sort': 'sort',
  1589 		'theme-install.php?upload': 'upload',
       
  1590 		'theme-install.php?search=:query': 'search',
  1893 		'theme-install.php?search=:query': 'search',
  1591 		'theme-install.php': 'sort'
  1894 		'theme-install.php': 'sort'
  1592 	},
  1895 	},
  1593 
  1896 
  1594 	baseUrl: function( url ) {
  1897 	baseUrl: function( url ) {
  1598 	themePath: '?theme=',
  1901 	themePath: '?theme=',
  1599 	browsePath: '?browse=',
  1902 	browsePath: '?browse=',
  1600 	searchPath: '?search=',
  1903 	searchPath: '?search=',
  1601 
  1904 
  1602 	search: function( query ) {
  1905 	search: function( query ) {
  1603 		$( '.wp-filter-search' ).val( query );
  1906 		$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
  1604 	},
  1907 	},
  1605 
  1908 
  1606 	navigate: function() {
  1909 	navigate: navigateRouter
  1607 		if ( Backbone.history._hasPushState ) {
       
  1608 			Backbone.Router.prototype.navigate.apply( this, arguments );
       
  1609 		}
       
  1610 	}
       
  1611 });
  1910 });
  1612 
  1911 
  1613 
  1912 
  1614 themes.RunInstaller = {
  1913 themes.RunInstaller = {
  1615 
  1914 
  1622 		});
  1921 		});
  1623 
  1922 
  1624 		// Render results
  1923 		// Render results
  1625 		this.render();
  1924 		this.render();
  1626 
  1925 
       
  1926 		// Start debouncing user searches after Backbone.history.start().
       
  1927 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
  1627 	},
  1928 	},
  1628 
  1929 
  1629 	render: function() {
  1930 	render: function() {
  1630 
  1931 
  1631 		// Render results
  1932 		// Render results
  1632 		this.view.render();
  1933 		this.view.render();
  1633 		this.routes();
  1934 		this.routes();
  1634 
  1935 
       
  1936 		if ( Backbone.History.started ) {
       
  1937 			Backbone.history.stop();
       
  1938 		}
  1635 		Backbone.history.start({
  1939 		Backbone.history.start({
  1636 			root: themes.data.settings.adminUrl,
  1940 			root: themes.data.settings.adminUrl,
  1637 			pushState: true,
  1941 			pushState: true,
  1638 			hashChange: false
  1942 			hashChange: false
  1639 		});
  1943 		});
  1648 		themes.router = new themes.InstallerRouter();
  1952 		themes.router = new themes.InstallerRouter();
  1649 
  1953 
  1650 		// Handles `theme` route event
  1954 		// Handles `theme` route event
  1651 		// Queries the API for the passed theme slug
  1955 		// Queries the API for the passed theme slug
  1652 		themes.router.on( 'route:preview', function( slug ) {
  1956 		themes.router.on( 'route:preview', function( slug ) {
  1653 			request.theme = slug;
  1957 
  1654 			self.view.collection.query( request );
  1958 			// Remove existing handlers.
       
  1959 			if ( themes.preview ) {
       
  1960 				themes.preview.undelegateEvents();
       
  1961 				themes.preview.unbind();
       
  1962 			}
       
  1963 
       
  1964 			// If the theme preview is active, set the current theme.
       
  1965 			if ( self.view.view.theme && self.view.view.theme.preview ) {
       
  1966 				self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } );
       
  1967 				self.view.view.theme.preview();
       
  1968 			} else {
       
  1969 
       
  1970 				// Select the theme by slug.
       
  1971 				request.theme = slug;
       
  1972 				self.view.collection.query( request );
       
  1973 				self.view.collection.trigger( 'update' );
       
  1974 
       
  1975 				// Open the theme preview.
       
  1976 				self.view.collection.once( 'query:success', function() {
       
  1977 					$( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
       
  1978 				});
       
  1979 
       
  1980 			}
  1655 		});
  1981 		});
  1656 
  1982 
  1657 		// Handles sorting / browsing routes
  1983 		// Handles sorting / browsing routes
  1658 		// Also handles the root URL triggering a sort request
  1984 		// Also handles the root URL triggering a sort request
  1659 		// for `featured`, the default view
  1985 		// for `featured`, the default view
  1660 		themes.router.on( 'route:sort', function( sort ) {
  1986 		themes.router.on( 'route:sort', function( sort ) {
  1661 			if ( ! sort ) {
  1987 			if ( ! sort ) {
  1662 				sort = 'featured';
  1988 				sort = 'featured';
       
  1989 				themes.router.navigate( themes.router.baseUrl( '?browse=featured' ), { replace: true } );
  1663 			}
  1990 			}
  1664 			self.view.sort( sort );
  1991 			self.view.sort( sort );
  1665 			self.view.trigger( 'theme:close' );
  1992 
  1666 		});
  1993 			// Close the preview if open.
  1667 
  1994 			if ( themes.preview ) {
  1668 		// Support the `upload` route by going straight to upload section
  1995 				themes.preview.close();
  1669 		themes.router.on( 'route:upload', function() {
  1996 			}
  1670 			$( 'a.upload' ).trigger( 'click' );
       
  1671 		});
  1997 		});
  1672 
  1998 
  1673 		// The `search` route event. The router populates the input field.
  1999 		// The `search` route event. The router populates the input field.
  1674 		themes.router.on( 'route:search', function() {
  2000 		themes.router.on( 'route:search', function() {
  1675 			$( '.wp-filter-search' ).focus().trigger( 'keyup' );
  2001 			$( '.wp-filter-search' ).focus().trigger( 'keyup' );
  1688 	if ( themes.isInstall ) {
  2014 	if ( themes.isInstall ) {
  1689 		themes.RunInstaller.init();
  2015 		themes.RunInstaller.init();
  1690 	} else {
  2016 	} else {
  1691 		themes.Run.init();
  2017 		themes.Run.init();
  1692 	}
  2018 	}
       
  2019 
       
  2020 	// Update the return param just in time.
       
  2021 	$( document.body ).on( 'click', '.load-customize', function() {
       
  2022 		var link = $( this ), urlParser = document.createElement( 'a' );
       
  2023 		urlParser.href = link.prop( 'href' );
       
  2024 		urlParser.search = $.param( _.extend(
       
  2025 			wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ),
       
  2026 			{
       
  2027 				'return': window.location.href
       
  2028 			}
       
  2029 		) );
       
  2030 		link.prop( 'href', urlParser.href );
       
  2031 	});
  1693 
  2032 
  1694 	$( '.broken-themes .delete-theme' ).on( 'click', function() {
  2033 	$( '.broken-themes .delete-theme' ).on( 'click', function() {
  1695 		return confirm( _wpThemeSettings.settings.confirmDelete );
  2034 		return confirm( _wpThemeSettings.settings.confirmDelete );
  1696 	});
  2035 	});
  1697 });
  2036 });