+ // Pressing the left arrow key fires a theme:previous event
+ if ( event.keyCode === 37 ) {
+ self.overlay.previousTheme();
+ }
+
+ // Pressing the escape key fires a theme:collapse event
+ if ( event.keyCode === 27 ) {
+ self.overlay.collapse( event );
+ }
+ });
+ },
+
+ // Manages rendering of theme pages
+ // and keeping theme count in sync
+ render: function() {
+ // Clear the DOM, please
+ this.$el.html( '' );
+
+ // If the user doesn't have switch capabilities
+ // or there is only one theme in the collection
+ // render the detailed view of the active theme
+ if ( themes.data.themes.length === 1 ) {
+
+ // Constructs the view
+ this.singleTheme = new themes.view.Details({
+ model: this.collection.models[0]
+ });
+
+ // Render and apply a 'single-theme' class to our container
+ this.singleTheme.render();
+ this.$el.addClass( 'single-theme' );
+ this.$el.append( this.singleTheme.el );
+ }
+
+ // Generate the themes
+ // Using page instance
+ // While checking the collection has items
+ if ( this.options.collection.size() > 0 ) {
+ this.renderThemes( this.parent.page );
+ }
+
+ // Display a live theme count for the collection
+ this.count.text( this.collection.count ? this.collection.count : this.collection.length );
+ },
+
+ // Iterates through each instance of the collection
+ // and renders each theme module
+ renderThemes: function( page ) {
+ var self = this;
+
+ self.instance = self.collection.paginate( page );
+
+ // If we have no more themes bail
+ if ( self.instance.size() === 0 ) {
+ // Fire a no-more-themes event.
+ this.parent.trigger( 'theme:end' );
+ return;
+ }
+
+ // Make sure the add-new stays at the end
+ if ( page >= 1 ) {
+ $( '.add-new-theme' ).remove();
+ }
+
+ // Loop through the themes and setup each theme view
+ self.instance.each( function( theme ) {
+ self.theme = new themes.view.Theme({
+ model: theme,
+ parent: self
+ });
+
+ // Render the views...
+ self.theme.render();
+ // and append them to div.themes
+ self.$el.append( self.theme.el );
+
+ // Binds to theme:expand to show the modal box
+ // with the theme details
+ self.listenTo( self.theme, 'theme:expand', self.expand, self );
+ });
+
+ // 'Add new theme' element shown at the end of the grid
+ if ( themes.data.settings.canInstall ) {
+ 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>' );
+ }
+
+ this.parent.page++;
+ },
+
+ // Grabs current theme and puts it at the beginning of the collection
+ currentTheme: function() {
+ var self = this,
+ current;
+
+ current = self.collection.findWhere({ active: true });
+
+ // Move the active theme to the beginning of the collection
+ if ( current ) {
+ self.collection.remove( current );
+ self.collection.add( current, { at:0 } );
+ }
+ },
+
+ // Sets current view
+ setView: function( view ) {
+ return view;
+ },
+
+ // Renders the overlay with the ThemeDetails view
+ // Uses the current model data
+ expand: function( id ) {
+ var self = this;
+
+ // Set the current theme model
+ this.model = self.collection.get( id );
+
+ // Trigger a route update for the current model
+ themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
+
+ // Sets this.view to 'detail'
+ this.setView( 'detail' );
+ $( 'body' ).addClass( 'modal-open' );
+
+ // Set up the theme details view
+ this.overlay = new themes.view.Details({
+ model: self.model
+ });
+
+ this.overlay.render();
+ this.$overlay.html( this.overlay.el );
+
+ // Bind to theme:next and theme:previous
+ // triggered by the arrow keys
+ //
+ // Keep track of the current model so we
+ // can infer an index position
+ this.listenTo( this.overlay, 'theme:next', function() {
+ // Renders the next theme on the overlay
+ self.next( [ self.model.cid ] );
+
+ })
+ .listenTo( this.overlay, 'theme:previous', function() {
+ // Renders the previous theme on the overlay
+ self.previous( [ self.model.cid ] );
+ });
+ },
+
+ // This method renders the next theme on the overlay modal
+ // based on the current position in the collection
+ // @params [model cid]
+ next: function( args ) {
+ var self = this,
+ model, nextModel;
+
+ // Get the current theme
+ model = self.collection.get( args[0] );
+ // Find the next model within the collection
+ nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
+
+ // Sanity check which also serves as a boundary test
+ if ( nextModel !== undefined ) {
+
+ // We have a new theme...
+ // Close the overlay
+ this.overlay.closeOverlay();
+
+ // Trigger a route update for the current model
+ self.theme.trigger( 'theme:expand', nextModel.cid );
+
+ }
+ },
+
+ // This method renders the previous theme on the overlay modal
+ // based on the current position in the collection
+ // @params [model cid]
+ previous: function( args ) {
+ var self = this,
+ model, previousModel;
+
+ // Get the current theme
+ model = self.collection.get( args[0] );
+ // Find the previous model within the collection
+ previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
+
+ if ( previousModel !== undefined ) {
+
+ // We have a new theme...
+ // Close the overlay
+ this.overlay.closeOverlay();
+
+ // Trigger a route update for the current model
+ self.theme.trigger( 'theme:expand', previousModel.cid );
+
+ }
+ }
+});
+
+// Search input view controller.
+themes.view.Search = wp.Backbone.View.extend({
+
+ tagName: 'input',
+ className: 'wp-filter-search',
+ id: 'wp-filter-search-input',
+ searching: false,
+
+ attributes: {
+ placeholder: l10n.searchPlaceholder,
+ type: 'search'
+ },
+
+ events: {
+ 'input': 'search',
+ 'keyup': 'search',
+ 'change': 'search',
+ 'search': 'search',
+ 'blur': 'pushState'
+ },
+
+ initialize: function( options ) {
+
+ this.parent = options.parent;
+
+ this.listenTo( this.parent, 'theme:close', function() {
+ this.searching = false;
+ } );
+
+ },
+
+ // Runs a search on the theme collection.
+ search: function( event ) {
+ var options = {};
+
+ // Clear on escape.
+ if ( event.type === 'keyup' && event.which === 27 ) {
+ event.target.value = '';
+ }
+
+ // Lose input focus when pressing enter
+ if ( event.which === 13 ) {
+ this.$el.trigger( 'blur' );
+ }
+
+ this.collection.doSearch( event.target.value );
+
+ // if search is initiated and key is not return
+ if ( this.searching && event.which !== 13 ) {
+ options.replace = true;
+ } else {
+ this.searching = true;
+ }
+
+ // Update the URL hash
+ if ( event.target.value ) {
+ themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
+ } else {
+ themes.router.navigate( themes.router.baseUrl( '' ) );
+ }
+ },
+
+ pushState: function( event ) {
+ var url = themes.router.baseUrl( '' );
+
+ if ( event.target.value ) {
+ url = themes.router.baseUrl( themes.router.searchPath + event.target.value );
+ }
+
+ this.searching = false;
+ themes.router.navigate( url );
+
+ }
+});
+
+// Sets up the routes events for relevant url queries
+// Listens to [theme] and [search] params
+themes.Router = Backbone.Router.extend({
+
+ routes: {
+ 'themes.php?theme=:slug': 'theme',
+ 'themes.php?search=:query': 'search',
+ 'themes.php?s=:query': 'search',
+ 'themes.php': 'themes',
+ '': 'themes'
+ },
+
+ baseUrl: function( url ) {
+ return 'themes.php' + url;
+ },
+
+ themePath: '?theme=',
+ searchPath: '?search=',
+
+ search: function( query ) {
+ $( '.wp-filter-search' ).val( query );
+ },
+
+ themes: function() {
+ $( '.wp-filter-search' ).val( '' );
+ },
+
+ navigate: function() {
+ if ( Backbone.history._hasPushState ) {
+ Backbone.Router.prototype.navigate.apply( this, arguments );
+ }
+ }
+
+});
+
+// Execute and setup the application
+themes.Run = {
+ init: function() {
+ // Initializes the blog's theme library view
+ // Create a new collection with data
+ this.themes = new themes.Collection( themes.data.themes );
+
+ // Set up the view
+ this.view = new themes.view.Appearance({
+ collection: this.themes
+ });
+
+ this.render();
+ },
+
+ render: function() {
+
+ // Render results
+ this.view.render();
+ this.routes();
+
+ Backbone.history.start({
+ root: themes.data.settings.adminUrl,
+ pushState: true,
+ hashChange: false
+ });
+ },
+
+ routes: function() {
+ var self = this;
+ // Bind to our global thx object
+ // so that the object is available to sub-views
+ themes.router = new themes.Router();
+
+ // Handles theme details route event
+ themes.router.on( 'route:theme', function( slug ) {
+ self.view.view.expand( slug );
+ });
+
+ themes.router.on( 'route:themes', function() {
+ self.themes.doSearch( '' );
+ self.view.trigger( 'theme:close' );
+ });
+
+ // Handles search route event
+ themes.router.on( 'route:search', function() {
+ $( '.wp-filter-search' ).trigger( 'keyup' );
+ });
+
+ this.extraRoutes();
+ },
+
+ extraRoutes: function() {
+ return false;
+ }
+};
+
+// Extend the main Search view
+themes.view.InstallerSearch = themes.view.Search.extend({
+
+ events: {
+ 'keyup': 'search'
+ },
+
+ // Handles Ajax request for searching through themes in public repo
+ search: function( event ) {
+
+ // Tabbing or reverse tabbing into the search input shouldn't trigger a search
+ if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
+ return;
+ }
+
+ this.collection = this.options.parent.view.collection;
+
+ // Clear on escape.
+ if ( event.type === 'keyup' && event.which === 27 ) {
+ event.target.value = '';
+ }
+
+ _.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value );
+ },
+
+ doSearch: _.debounce( function( value ) {
+ var request = {};
+
+ request.search = value;
+
+ // Intercept an [author] search.
+ //
+ // If input value starts with `author:` send a request
+ // for `author` instead of a regular `search`
+ if ( value.substring( 0, 7 ) === 'author:' ) {
+ request.search = '';
+ request.author = value.slice( 7 );
+ }
+
+ // Intercept a [tag] search.
+ //
+ // If input value starts with `tag:` send a request
+ // for `tag` instead of a regular `search`
+ if ( value.substring( 0, 4 ) === 'tag:' ) {
+ request.search = '';
+ request.tag = [ value.slice( 4 ) ];
+ }
+
+ $( '.filter-links li > a.current' ).removeClass( 'current' );
+ $( 'body' ).removeClass( 'show-filters filters-applied' );
+
+ // Get the themes by sending Ajax POST request to api.wordpress.org/themes
+ // or searching the local cache
+ this.collection.query( request );
+
+ // Set route
+ themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + value ), { replace: true } );
+ }, 300 )
+});
+
+themes.view.Installer = themes.view.Appearance.extend({
+
+ el: '#wpbody-content .wrap',
+
+ // Register events for sorting and filters in theme-navigation
+ events: {
+ 'click .filter-links li > a': 'onSort',
+ 'click .theme-filter': 'onFilter',
+ 'click .drawer-toggle': 'moreFilters',
+ 'click .filter-drawer .apply-filters': 'applyFilters',
+ 'click .filter-group [type="checkbox"]': 'addFilter',
+ 'click .filter-drawer .clear-filters': 'clearFilters',
+ 'click .filtered-by': 'backToFilters'
+ },
+
+ // Initial render method
+ render: function() {
+ var self = this;
+
+ this.search();
+ this.uploader();
+
+ this.collection = new themes.Collection();
+
+ // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
+ this.listenTo( this, 'theme:end', function() {
+
+ // Make sure we are not already loading
+ if ( self.collection.loadingThemes ) {