WordPress.org

Make WordPress Core

Changeset 27830


Ignore:
Timestamp:
03/28/2014 09:48:52 PM (5 years ago)
Author:
nacin
Message:

Theme Installer: Caching and paginating of API requests.

props matveb.
see #27055.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/js/theme.js

    r27804 r27830  
    214214
    215215        return collection;
    216     }
    217 });
    218 
    219 // This is the view that controls each theme item
    220 // that will be displayed on the screen
    221 themes.view.Theme = wp.Backbone.View.extend({
    222 
    223     // Wrap theme data on a div.theme element
    224     className: 'theme',
    225 
    226     // Reflects which theme view we have
    227     // 'grid' (default) or 'detail'
    228     state: 'grid',
    229 
    230     // The HTML template for each element to be rendered
    231     html: themes.template( 'theme' ),
    232 
    233     events: {
    234         'click': themes.isInstall ? 'preview': 'expand',
    235         'click .preview': 'preview',
    236         'keydown': themes.isInstall ? 'preview': 'expand',
    237         'touchend': themes.isInstall ? 'preview': 'expand',
    238         'keyup': 'addFocus',
    239         'touchmove': 'preventExpand'
    240     },
    241 
    242     touchDrag: false,
    243 
    244     render: function() {
    245         var data = this.model.toJSON();
    246         // Render themes using the html template
    247         this.$el.html( this.html( data ) ).attr({
    248             tabindex: 0,
    249             'aria-describedby' : data.id + '-action ' + data.id + '-name'
    250         });
    251 
    252         // Renders active theme styles
    253         this.activeTheme();
    254 
    255         if ( this.model.get( 'displayAuthor' ) ) {
    256             this.$el.addClass( 'display-author' );
    257         }
    258     },
    259 
    260     // Adds a class to the currently active theme
    261     // and to the overlay in detailed view mode
    262     activeTheme: function() {
    263         if ( this.model.get( 'active' ) ) {
    264             this.$el.addClass( 'active' );
    265         }
    266     },
    267 
    268     // Add class of focus to the theme we are focused on.
    269     addFocus: function() {
    270         var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
    271 
    272         $('.theme.focus').removeClass('focus');
    273         $themeToFocus.addClass('focus');
    274     },
    275 
    276     // Single theme overlay screen
    277     // It's shown when clicking a theme
    278     expand: function( event ) {
    279         var self = this;
    280 
    281         event = event || window.event;
    282 
    283         // 'enter' and 'space' keys expand the details view when a theme is :focused
    284         if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
    285             return;
    286         }
    287 
    288         // Bail if the user scrolled on a touch device
    289         if ( this.touchDrag === true ) {
    290             return this.touchDrag = false;
    291         }
    292 
    293         // Prevent the modal from showing when the user clicks
    294         // one of the direct action buttons
    295         if ( $( event.target ).is( '.theme-actions a' ) ) {
    296             return;
    297         }
    298 
    299         // Set focused theme to current element
    300         themes.focusedTheme = this.$el;
    301 
    302         this.trigger( 'theme:expand', self.model.cid );
    303     },
    304 
    305     preventExpand: function() {
    306         this.touchDrag = true;
    307     },
    308 
    309     preview: function( event ) {
    310         // Bail if the user scrolled on a touch device
    311         if ( this.touchDrag === true ) {
    312             return this.touchDrag = false;
    313         }
    314 
    315         // 'enter' and 'space' keys expand the details view when a theme is :focused
    316         if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
    317             return;
    318         }
    319 
    320         // pressing enter while focused on the buttons shouldn't open the preview
    321         if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
    322             return;
    323         }
    324 
    325         event.preventDefault();
    326 
    327         event = event || window.event;
    328 
    329         var preview = new themes.view.Preview({
    330             model: this.model
    331         });
    332 
    333         preview.render();
    334         $( 'div.wrap' ).append( preview.el );
    335     }
    336 });
    337 
    338 // Theme Details view
    339 // Set ups a modal overlay with the expanded theme data
    340 themes.view.Details = wp.Backbone.View.extend({
    341 
    342     // Wrap theme data on a div.theme element
    343     className: 'theme-overlay',
    344 
    345     events: {
    346         'click': 'collapse',
    347         'click .delete-theme': 'deleteTheme',
    348         'click .left': 'previousTheme',
    349         'click .right': 'nextTheme'
    350     },
    351 
    352     // The HTML template for the theme overlay
    353     html: themes.template( 'theme-single' ),
    354 
    355     render: function() {
    356         var data = this.model.toJSON();
    357         this.$el.html( this.html( data ) );
    358         // Renders active theme styles
    359         this.activeTheme();
    360         // Set up navigation events
    361         this.navigation();
    362         // Checks screenshot size
    363         this.screenshotCheck( this.$el );
    364         // Contain "tabbing" inside the overlay
    365         this.containFocus( this.$el );
    366     },
    367 
    368     // Adds a class to the currently active theme
    369     // and to the overlay in detailed view mode
    370     activeTheme: function() {
    371         // Check the model has the active property
    372         this.$el.toggleClass( 'active', this.model.get( 'active' ) );
    373     },
    374 
    375     // Keeps :focus within the theme details elements
    376     containFocus: function( $el ) {
    377         var $target;
    378 
    379         // Move focus to the primary action
    380         _.delay( function() {
    381             $( '.theme-wrap a.button-primary:visible' ).focus();
    382         }, 500 );
    383 
    384         $el.on( 'keydown.wp-themes', function( event ) {
    385 
    386             // Tab key
    387             if ( event.which === 9 ) {
    388                 $target = $( event.target );
    389 
    390                 // Keep focus within the overlay by making the last link on theme actions
    391                 // switch focus to button.left on tabbing and vice versa
    392                 if ( $target.is( 'button.left' ) && event.shiftKey ) {
    393                     $el.find( '.theme-actions a:last-child' ).focus();
    394                     event.preventDefault();
    395                 } else if ( $target.is( '.theme-actions a:last-child' ) ) {
    396                     $el.find( 'button.left' ).focus();
    397                     event.preventDefault();
    398                 }
     216    },
     217
     218    // Handles requests for more themes
     219    // and caches results
     220    //
     221    // When we are missing a cache object we fire an apiCall()
     222    // which triggers events of `query:success` or `query:fail`
     223    query: function( request ) {
     224        /**
     225         * @static
     226         * @type Array
     227         */
     228        var queries = this.queries,
     229            self = this,
     230            query, isPaginated, count;
     231
     232        // Search the query cache for matches.
     233        query = _.find( queries, function( query ) {
     234            return _.isEqual( query.request, request );
     235        });
     236
     237        // If the request matches the stored currentQuery.request
     238        // it means we have a paginated request.
     239        isPaginated = _.has( request, 'page' );
     240
     241        // Reset the internal api page counter for non paginated queries.
     242        if ( ! isPaginated ) {
     243            this.currentQuery.page = 1;
     244        }
     245
     246        // Otherwise, send a new API call and add it to the cache.
     247        if ( ! query ) {
     248            query = this.apiCall( request ).done( function( data ) {
     249                // Update the collection with the queried data.
     250                self.reset( data.themes );
     251                count = data.info.results;
     252
     253                // Trigger a collection refresh event
     254                // and a `query:success` event with a `count` argument.
     255                self.trigger( 'update' );
     256                self.trigger( 'query:success', count );
     257
     258                // Store the results and the query request
     259                queries.push( { themes: data.themes, request: request } );
     260            }).fail( function() {
     261                self.trigger( 'query:fail' );
     262            });
     263        } else {
     264            // If it's a paginated request we need to fetch more themes...
     265            if ( isPaginated ) {
     266                return this.apiCall( request, isPaginated ).done( function( data ) {
     267                    // Add the new themes to the current collection
     268                    // @todo update counter
     269                    self.add( data.themes );
     270                    self.trigger( 'query:success' );
     271
     272                }).fail( function() {
     273                    self.trigger( 'query:fail' );
     274                });
    399275            }
    400         });
    401     },
    402 
    403     // Single theme overlay screen
    404     // It's shown when clicking a theme
    405     collapse: function( event ) {
    406         var self = this,
    407             scroll;
    408 
    409         event = event || window.event;
    410 
    411         // Prevent collapsing detailed view when there is only one theme available
    412         if ( themes.data.themes.length === 1 ) {
    413             return;
    414         }
    415 
    416         // Detect if the click is inside the overlay
    417         // and don't close it unless the target was
    418         // the div.back button
    419         if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
    420 
    421             // Add a temporary closing class while overlay fades out
    422             $( 'body' ).addClass( 'closing-overlay' );
    423 
    424             // With a quick fade out animation
    425             this.$el.fadeOut( 130, function() {
    426                 // Clicking outside the modal box closes the overlay
    427                 $( 'body' ).removeClass( 'closing-overlay' );
    428                 // Handle event cleanup
    429                 self.closeOverlay();
    430 
    431                 // Get scroll position to avoid jumping to the top
    432                 scroll = document.body.scrollTop;
    433 
    434                 // Clean the url structure
    435                 themes.router.navigate( themes.router.baseUrl( '' ) );
    436 
    437                 // Restore scroll position
    438                 document.body.scrollTop = scroll;
    439 
    440                 // Return focus to the theme div
    441                 if ( themes.focusedTheme ) {
    442                     themes.focusedTheme.focus();
    443                 }
    444             });
    445         }
    446     },
    447 
    448     // Handles .disabled classes for next/previous buttons
    449     navigation: function() {
    450 
    451         // Disable Left/Right when at the start or end of the collection
    452         if ( this.model.cid === this.model.collection.at(0).cid ) {
    453             this.$el.find( '.left' ).addClass( 'disabled' );
    454         }
    455         if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
    456             this.$el.find( '.right' ).addClass( 'disabled' );
    457         }
    458     },
    459 
    460     // Performs the actions to effectively close
    461     // the theme details overlay
    462     closeOverlay: function() {
    463         $( 'body' ).removeClass( 'theme-overlay-open' );
    464         this.remove();
    465         this.unbind();
    466         this.trigger( 'theme:collapse' );
    467     },
    468 
    469     // Confirmation dialoge for deleting a theme
    470     deleteTheme: function() {
    471         return confirm( themes.data.settings.confirmDelete );
    472     },
    473 
    474     nextTheme: function() {
    475         var self = this;
    476         self.trigger( 'theme:next', self.model.cid );
    477     },
    478 
    479     previousTheme: function() {
    480         var self = this;
    481         self.trigger( 'theme:previous', self.model.cid );
    482     },
    483 
    484     // Checks if the theme screenshot is the old 300px width version
    485     // and adds a corresponding class if it's true
    486     screenshotCheck: function( el ) {
    487         var screenshot, image;
    488 
    489         screenshot = el.find( '.screenshot img' );
    490         image = new Image();
    491         image.src = screenshot.attr( 'src' );
    492 
    493         // Width check
    494         if ( image.width && image.width <= 300 ) {
    495             el.addClass( 'small-screenshot' );
    496         }
    497     }
    498 });
    499 
    500 // Theme Preview view
    501 // Set ups a modal overlay with the expanded theme data
    502 themes.view.Preview = wp.Backbone.View.extend({
    503 
    504     className: 'wp-full-overlay expanded',
    505     el: '#theme-installer',
    506 
    507     events: {
    508         'click .close-full-overlay': 'close',
    509         'click .collapse-sidebar': 'collapse'
    510     },
    511 
    512     // The HTML template for the theme preview
    513     html: themes.template( 'theme-preview' ),
    514 
    515     render: function() {
    516         var data = this.model.toJSON();
    517         this.$el.html( this.html( data ) );
    518 
    519         themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.get( 'id' ) ), { replace: true } );
    520 
    521         this.$el.fadeIn( 200, function() {
    522             $( 'body' ).addClass( 'theme-installer-active full-overlay-active' );
    523         });
    524     },
    525 
    526     close: function() {
    527         this.$el.fadeOut( 200, function() {
    528             $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
    529         });
    530 
    531         themes.router.navigate( themes.router.baseUrl( '' ) );
    532         return false;
    533     },
    534 
    535     collapse: function() {
    536         this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
    537         return false;
    538     }
    539 });
    540 
    541 // Controls the rendering of div.themes,
    542 // a wrapper that will hold all the theme elements
    543 themes.view.Themes = wp.Backbone.View.extend({
    544 
    545     className: 'themes',
    546     $overlay: $( 'div.theme-overlay' ),
    547 
    548     // Number to keep track of scroll position
    549     // while in theme-overlay mode
    550     index: 0,
    551 
    552     // The theme count element
    553     count: $( '.theme-count' ),
    554 
    555     initialize: function( options ) {
    556         var self = this;
    557 
    558         // Set up parent
    559         this.parent = options.parent;
    560 
    561         // Set current view to [grid]
    562         this.setView( 'grid' );
    563 
    564         // Move the active theme to the beginning of the collection
    565         self.currentTheme();
    566 
    567         // When the collection is updated by user input...
    568         this.listenTo( self.collection, 'update', function() {
    569             self.parent.page = 0;
    570             self.currentTheme();
    571             self.render( this );
    572         });
    573 
    574         this.listenTo( this.parent, 'theme:scroll', function() {
    575             self.renderThemes( self.parent.page );
    576         });
    577 
    578         this.listenTo( this.parent, 'theme:close', function() {
    579             if ( self.overlay ) {
    580                 self.overlay.closeOverlay();
    581             }
    582         } );
    583 
    584         // Bind keyboard events.
    585         $('body').on( 'keyup', function( event ) {
    586             if ( ! self.overlay ) {
    587                 return;
    588             }
    589 
    590             // Pressing the right arrow key fires a theme:next event
    591             if ( event.keyCode === 39 ) {
    592                 self.overlay.nextTheme();
    593             }
    594 
    595             // Pressing the left arrow key fires a theme:previous event
    596             if ( event.keyCode === 37 ) {
    597                 self.overlay.previousTheme();
    598             }
    599 
    600             // Pressing the escape key fires a theme:collapse event
    601             if ( event.keyCode === 27 ) {
    602                 self.overlay.collapse( event );
    603             }
    604         });
    605     },
    606 
    607     // Manages rendering of theme pages
    608     // and keeping theme count in sync
    609     render: function() {
    610         // Clear the DOM, please
    611         this.$el.html( '' );
    612 
    613         // If the user doesn't have switch capabilities
    614         // or there is only one theme in the collection
    615         // render the detailed view of the active theme
    616         if ( themes.data.themes.length === 1 ) {
    617 
    618             // Constructs the view
    619             this.singleTheme = new themes.view.Details({
    620                 model: this.collection.models[0]
    621             });
    622 
    623             // Render and apply a 'single-theme' class to our container
    624             this.singleTheme.render();
    625             this.$el.addClass( 'single-theme' );
    626             this.$el.append( this.singleTheme.el );
    627         }
    628 
    629         // Generate the themes
    630         // Using page instance
    631         this.renderThemes( this.parent.page );
    632 
    633         // Display a live theme count for the collection
    634         this.count.text( this.collection.length );
    635     },
    636 
    637     // Iterates through each instance of the collection
    638     // and renders each theme module
    639     renderThemes: function( page ) {
    640         var self = this;
    641 
    642         self.instance = self.collection.paginate( page );
    643 
    644         // If we have no more themes bail
    645         if ( self.instance.length === 0 ) {
    646             return;
    647         }
    648 
    649         // Make sure the add-new stays at the end
    650         if ( page >= 1 ) {
    651             $( '.add-new-theme' ).remove();
    652         }
    653 
    654         // Loop through the themes and setup each theme view
    655         self.instance.each( function( theme ) {
    656             self.theme = new themes.view.Theme({
    657                 model: theme
    658             });
    659 
    660             // Render the views...
    661             self.theme.render();
    662             // and append them to div.themes
    663             self.$el.append( self.theme.el );
    664 
    665             // Binds to theme:expand to show the modal box
    666             // with the theme details
    667             self.listenTo( self.theme, 'theme:expand', self.expand, self );
    668         });
    669 
    670         // 'Add new theme' element shown at the end of the grid
    671         if ( themes.data.settings.canInstall ) {
    672             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>' );
    673         }
    674 
    675         this.parent.page++;
    676     },
    677 
    678     // Grabs current theme and puts it at the beginning of the collection
    679     currentTheme: function() {
    680         var self = this,
    681             current;
    682 
    683         current = self.collection.findWhere({ active: true });
    684 
    685         // Move the active theme to the beginning of the collection
    686         if ( current ) {
    687             self.collection.remove( current );
    688             self.collection.add( current, { at:0 } );
    689         }
    690     },
    691 
    692     // Sets current view
    693     setView: function( view ) {
    694         return view;
    695     },
    696 
    697     // Renders the overlay with the ThemeDetails view
    698     // Uses the current model data
    699     expand: function( id ) {
    700         var self = this;
    701 
    702         // Set the current theme model
    703         this.model = self.collection.get( id );
    704 
    705         // Trigger a route update for the current model
    706         themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.id ) );
    707 
    708         // Sets this.view to 'detail'
    709         this.setView( 'detail' );
    710         $( 'body' ).addClass( 'theme-overlay-open' );
    711 
    712         // Set up the theme details view
    713         this.overlay = new themes.view.Details({
    714             model: self.model
    715         });
    716 
    717         this.overlay.render();
    718         this.$overlay.html( this.overlay.el );
    719 
    720         // Bind to theme:next and theme:previous
    721         // triggered by the arrow keys
    722         //
    723         // Keep track of the current model so we
    724         // can infer an index position
    725         this.listenTo( this.overlay, 'theme:next', function() {
    726             // Renders the next theme on the overlay
    727             self.next( [ self.model.cid ] );
    728 
    729         })
    730         .listenTo( this.overlay, 'theme:previous', function() {
    731             // Renders the previous theme on the overlay
    732             self.previous( [ self.model.cid ] );
    733         });
    734     },
    735 
    736     // This method renders the next theme on the overlay modal
    737     // based on the current position in the collection
    738     // @params [model cid]
    739     next: function( args ) {
    740         var self = this,
    741             model, nextModel;
    742 
    743         // Get the current theme
    744         model = self.collection.get( args[0] );
    745         // Find the next model within the collection
    746         nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
    747 
    748         // Sanity check which also serves as a boundary test
    749         if ( nextModel !== undefined ) {
    750 
    751             // We have a new theme...
    752             // Close the overlay
    753             this.overlay.closeOverlay();
    754 
    755             // Trigger a route update for the current model
    756             self.theme.trigger( 'theme:expand', nextModel.cid );
    757 
    758         }
    759     },
    760 
    761     // This method renders the previous theme on the overlay modal
    762     // based on the current position in the collection
    763     // @params [model cid]
    764     previous: function( args ) {
    765         var self = this,
    766             model, previousModel;
    767 
    768         // Get the current theme
    769         model = self.collection.get( args[0] );
    770         // Find the previous model within the collection
    771         previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
    772 
    773         if ( previousModel !== undefined ) {
    774 
    775             // We have a new theme...
    776             // Close the overlay
    777             this.overlay.closeOverlay();
    778 
    779             // Trigger a route update for the current model
    780             self.theme.trigger( 'theme:expand', previousModel.cid );
    781 
    782         }
    783     }
    784 });
    785 
    786 // Search input view controller.
    787 themes.view.Search = wp.Backbone.View.extend({
    788 
    789     tagName: 'input',
    790     className: 'theme-search',
    791     id: 'theme-search-input',
    792     searching: false,
    793 
    794     attributes: {
    795         placeholder: l10n.searchPlaceholder,
    796         type: 'search'
    797     },
    798 
    799     events: {
    800         'input':  'search',
    801         'keyup':  'search',
    802         'change': 'search',
    803         'search': 'search',
    804         'blur':   'pushState'
    805     },
    806 
    807     initialize: function( options ) {
    808 
    809         this.parent = options.parent;
    810 
    811         this.listenTo( this.parent, 'theme:close', function() {
    812             this.searching = false;
    813         } );
    814 
    815     },
    816 
    817     // Runs a search on the theme collection.
    818     search: function( event ) {
    819         var options = {};
    820 
    821         // Clear on escape.
    822         if ( event.type === 'keyup' && event.which === 27 ) {
    823             event.target.value = '';
    824         }
    825 
    826         // Lose input focus when pressing enter
    827         if ( event.which === 13 ) {
    828             this.$el.trigger( 'blur' );
    829         }
    830 
    831         this.collection.doSearch( event.target.value );
    832 
    833         // if search is initiated and key is not return
    834         if ( this.searching && event.which !== 13 ) {
    835             options.replace = true;
    836         } else {
    837             this.searching = true;
    838         }
    839 
    840         // Update the URL hash
    841         if ( event.target.value ) {
    842             themes.router.navigate( themes.router.baseUrl( '?search=' + event.target.value ), options );
    843         } else {
    844             themes.router.navigate( themes.router.baseUrl( '' ) );
    845         }
    846     },
    847 
    848     pushState: function( event ) {
    849         var url = themes.router.baseUrl( '' );
    850 
    851         if ( event.target.value ) {
    852             url = themes.router.baseUrl( '?search=' + event.target.value );
    853         }
    854 
    855         this.searching = false;
    856         themes.router.navigate( url );
    857 
    858     }
    859 });
    860 
    861 // Sets up the routes events for relevant url queries
    862 // Listens to [theme] and [search] params
    863 themes.Router = Backbone.Router.extend({
    864 
    865     routes: {
    866         'themes.php?theme=:slug': 'theme',
    867         'themes.php?search=:query': 'search',
    868         'themes.php?s=:query': 'search',
    869         'themes.php': 'themes',
    870         '': 'themes'
    871     },
    872 
    873     baseUrl: function( url ) {
    874         return 'themes.php' + url;
    875     },
    876 
    877     search: function( query ) {
    878         $( '.theme-search' ).val( query );
    879     },
    880 
    881     themes: function() {
    882         $( '.theme-search' ).val( '' );
    883     }
    884 
    885 });
    886 
    887 // Execute and setup the application
    888 themes.Run = {
    889     init: function() {
    890         // Initializes the blog's theme library view
    891         // Create a new collection with data
    892         this.themes = new themes.Collection( themes.data.themes );
    893 
    894         // Set up the view
    895         this.view = new themes.view.Appearance({
    896             collection: this.themes
    897         });
    898 
    899         this.render();
    900     },
    901 
    902     render: function() {
    903 
    904         // Render results
    905         this.view.render();
    906         this.routes();
    907 
    908         Backbone.history.start({
    909             root: themes.data.settings.adminUrl,
    910             pushState: true,
    911             hashChange: false
    912         });
    913     },
    914 
    915     routes: function() {
    916         var self = this;
    917         // Bind to our global thx object
    918         // so that the object is available to sub-views
    919         themes.router = new themes.Router();
    920 
    921         // Handles theme details route event
    922         themes.router.on( 'route:theme', function( slug ) {
    923             self.view.view.expand( slug );
    924         });
    925 
    926         themes.router.on( 'route:themes', function() {
    927             self.themes.doSearch( '' );
    928             self.view.trigger( 'theme:close' );
    929         });
    930 
    931         // Handles search route event
    932         themes.router.on( 'route:search', function( query ) {
    933             self.view.trigger( 'theme:close' );
    934             self.themes.doSearch( query );
    935         });
    936 
    937         this.extraRoutes();
    938     },
    939 
    940     extraRoutes: function() {
    941         return false;
    942     }
    943 };
    944 
    945 // Extend the main Search view
    946 themes.view.InstallerSearch =  themes.view.Search.extend({
    947 
    948     events: {
    949         'keyup': 'search'
    950     },
    951 
    952     // Handles Ajax request for searching through themes in public repo
    953     search: function( event ) {
    954 
    955         // Tabbing or reverse tabbing into the search input shouldn't trigger a search
    956         if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
    957             return;
    958         }
    959 
    960         this.collection = this.options.parent.view.collection;
    961 
    962         // Clear on escape.
    963         if ( event.type === 'keyup' && event.which === 27 ) {
    964             event.target.value = '';
    965         }
    966 
    967         _.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value );
    968     },
    969 
    970     doSearch: function( value ) {
    971         var request = {},
    972             self = this;
    973 
    974         request.search = value;
    975 
    976         // Intercept an [author] search.
    977         //
    978         // If input value starts with `author:` send a request
    979         // for `author` instead of a regular `search`
    980         if ( value.substring( 0, 7 ) === 'author:' ) {
    981             request.search = '';
    982             request.author = value.slice( 7 );
    983         }
    984 
    985         // Intercept a [tag] search.
    986         //
    987         // If input value starts with `tag:` send a request
    988         // for `tag` instead of a regular `search`
    989         if ( value.substring( 0, 4 ) === 'tag:' ) {
    990             request.search = '';
    991             request.tag = [ value.slice( 4 ) ];
    992         }
    993 
    994         // Send Ajax POST request to api.wordpress.org/themes
    995         themes.view.Installer.prototype.apiCall( request ).done( function( data ) {
    996                 // Update the collection with the queried data
    997                 self.collection.reset( data.themes );
    998                 // Trigger a collection refresh event to render the views
    999                 self.collection.trigger( 'update' );
    1000 
    1001                 // Un-spin it
    1002                 $( 'body' ).removeClass( 'loading-themes' );
    1003                 $( '.theme-browser' ).find( 'div.error' ).remove();
    1004         }).fail( function() {
    1005                 $( '.theme-browser' ).find( 'div.error' ).remove();
    1006                 $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
    1007         });
    1008 
    1009         return false;
    1010     }
    1011 });
    1012 
    1013 themes.view.Installer = themes.view.Appearance.extend({
    1014 
    1015     el: '#wpbody-content .wrap',
    1016 
    1017     // Register events for sorting and filters in theme-navigation
    1018     events: {
    1019         'click .theme-section': 'onSort',
    1020         'click .theme-filter': 'onFilter',
    1021         'click .more-filters': 'moreFilters',
    1022         'click [type="checkbox"]': 'addFilter'
     276
     277            // Only trigger an update event since we already have the themes
     278            // on our cached object
     279            this.reset( query.themes );
     280            this.trigger( 'update' );
     281        }
     282    },
     283
     284    // Local cache array for API queries
     285    queries: [],
     286
     287    // Keep track of current query so we can handle pagination
     288    currentQuery: {
     289        page: 1,
     290        request: {}
    1023291    },
    1024292
    1025293    // Send Ajax POST request to api.wordpress.org/themes
    1026     apiCall: function( request ) {
     294    apiCall: function( request, paginated ) {
     295        // Store current query request args
     296        // for later use with the event `theme:end`
     297        this.currentQuery.request = request;
     298
     299        // Ajax request to .org API
    1027300        return $.ajax({
    1028301            url: 'https://api.wordpress.org/themes/info/1.1/?action=query_themes',
     
    1037310                action: 'query_themes',
    1038311                request: _.extend({
    1039                     per_page: 36,
     312                    per_page: 72,
    1040313                    fields: {
    1041314                        description: true,
     
    1053326
    1054327            beforeSend: function() {
    1055                 // Spin it
    1056                 $( 'body' ).addClass( 'loading-themes' );
     328                if ( ! paginated ) {
     329                    // Spin it
     330                    $( 'body' ).addClass( 'loading-themes' );
     331                }
    1057332            }
    1058333        });
     334    }
     335});
     336
     337// This is the view that controls each theme item
     338// that will be displayed on the screen
     339themes.view.Theme = wp.Backbone.View.extend({
     340
     341    // Wrap theme data on a div.theme element
     342    className: 'theme',
     343
     344    // Reflects which theme view we have
     345    // 'grid' (default) or 'detail'
     346    state: 'grid',
     347
     348    // The HTML template for each element to be rendered
     349    html: themes.template( 'theme' ),
     350
     351    events: {
     352        'click': themes.isInstall ? 'preview': 'expand',
     353        'click .preview': 'preview',
     354        'keydown': themes.isInstall ? 'preview': 'expand',
     355        'touchend': themes.isInstall ? 'preview': 'expand',
     356        'keyup': 'addFocus',
     357        'touchmove': 'preventExpand'
     358    },
     359
     360    touchDrag: false,
     361
     362    render: function() {
     363        var data = this.model.toJSON();
     364        // Render themes using the html template
     365        this.$el.html( this.html( data ) ).attr({
     366            tabindex: 0,
     367            'aria-describedby' : data.id + '-action ' + data.id + '-name'
     368        });
     369
     370        // Renders active theme styles
     371        this.activeTheme();
     372
     373        if ( this.model.get( 'displayAuthor' ) ) {
     374            this.$el.addClass( 'display-author' );
     375        }
     376    },
     377
     378    // Adds a class to the currently active theme
     379    // and to the overlay in detailed view mode
     380    activeTheme: function() {
     381        if ( this.model.get( 'active' ) ) {
     382            this.$el.addClass( 'active' );
     383        }
     384    },
     385
     386    // Add class of focus to the theme we are focused on.
     387    addFocus: function() {
     388        var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
     389
     390        $('.theme.focus').removeClass('focus');
     391        $themeToFocus.addClass('focus');
     392    },
     393
     394    // Single theme overlay screen
     395    // It's shown when clicking a theme
     396    expand: function( event ) {
     397        var self = this;
     398
     399        event = event || window.event;
     400
     401        // 'enter' and 'space' keys expand the details view when a theme is :focused
     402        if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
     403            return;
     404        }
     405
     406        // Bail if the user scrolled on a touch device
     407        if ( this.touchDrag === true ) {
     408            return this.touchDrag = false;
     409        }
     410
     411        // Prevent the modal from showing when the user clicks
     412        // one of the direct action buttons
     413        if ( $( event.target ).is( '.theme-actions a' ) ) {
     414            return;
     415        }
     416
     417        // Set focused theme to current element
     418        themes.focusedTheme = this.$el;
     419
     420        this.trigger( 'theme:expand', self.model.cid );
     421    },
     422
     423    preventExpand: function() {
     424        this.touchDrag = true;
     425    },
     426
     427    preview: function( event ) {
     428        // Bail if the user scrolled on a touch device
     429        if ( this.touchDrag === true ) {
     430            return this.touchDrag = false;
     431        }
     432
     433        // 'enter' and 'space' keys expand the details view when a theme is :focused
     434        if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
     435            return;
     436        }
     437
     438        // pressing enter while focused on the buttons shouldn't open the preview
     439        if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
     440            return;
     441        }
     442
     443        event.preventDefault();
     444
     445        event = event || window.event;
     446
     447        var preview = new themes.view.Preview({
     448            model: this.model
     449        });
     450
     451        preview.render();
     452        $( 'div.wrap' ).append( preview.el );
     453    }
     454});
     455
     456// Theme Details view
     457// Set ups a modal overlay with the expanded theme data
     458themes.view.Details = wp.Backbone.View.extend({
     459
     460    // Wrap theme data on a div.theme element
     461    className: 'theme-overlay',
     462
     463    events: {
     464        'click': 'collapse',
     465        'click .delete-theme': 'deleteTheme',
     466        'click .left': 'previousTheme',
     467        'click .right': 'nextTheme'
     468    },
     469
     470    // The HTML template for the theme overlay
     471    html: themes.template( 'theme-single' ),
     472
     473    render: function() {
     474        var data = this.model.toJSON();
     475        this.$el.html( this.html( data ) );
     476        // Renders active theme styles
     477        this.activeTheme();
     478        // Set up navigation events
     479        this.navigation();
     480        // Checks screenshot size
     481        this.screenshotCheck( this.$el );
     482        // Contain "tabbing" inside the overlay
     483        this.containFocus( this.$el );
     484    },
     485
     486    // Adds a class to the currently active theme
     487    // and to the overlay in detailed view mode
     488    activeTheme: function() {
     489        // Check the model has the active property
     490        this.$el.toggleClass( 'active', this.model.get( 'active' ) );
     491    },
     492
     493    // Keeps :focus within the theme details elements
     494    containFocus: function( $el ) {
     495        var $target;
     496
     497        // Move focus to the primary action
     498        _.delay( function() {
     499            $( '.theme-wrap a.button-primary:visible' ).focus();
     500        }, 500 );
     501
     502        $el.on( 'keydown.wp-themes', function( event ) {
     503
     504            // Tab key
     505            if ( event.which === 9 ) {
     506                $target = $( event.target );
     507
     508                // Keep focus within the overlay by making the last link on theme actions
     509                // switch focus to button.left on tabbing and vice versa
     510                if ( $target.is( 'button.left' ) && event.shiftKey ) {
     511                    $el.find( '.theme-actions a:last-child' ).focus();
     512                    event.preventDefault();
     513                } else if ( $target.is( '.theme-actions a:last-child' ) ) {
     514                    $el.find( 'button.left' ).focus();
     515                    event.preventDefault();
     516                }
     517            }
     518        });
     519    },
     520
     521    // Single theme overlay screen
     522    // It's shown when clicking a theme
     523    collapse: function( event ) {
     524        var self = this,
     525            scroll;
     526
     527        event = event || window.event;
     528
     529        // Prevent collapsing detailed view when there is only one theme available
     530        if ( themes.data.themes.length === 1 ) {
     531            return;
     532        }
     533
     534        // Detect if the click is inside the overlay
     535        // and don't close it unless the target was
     536        // the div.back button
     537        if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
     538
     539            // Add a temporary closing class while overlay fades out
     540            $( 'body' ).addClass( 'closing-overlay' );
     541
     542            // With a quick fade out animation
     543            this.$el.fadeOut( 130, function() {
     544                // Clicking outside the modal box closes the overlay
     545                $( 'body' ).removeClass( 'closing-overlay' );
     546                // Handle event cleanup
     547                self.closeOverlay();
     548
     549                // Get scroll position to avoid jumping to the top
     550                scroll = document.body.scrollTop;
     551
     552                // Clean the url structure
     553                themes.router.navigate( themes.router.baseUrl( '' ) );
     554
     555                // Restore scroll position
     556                document.body.scrollTop = scroll;
     557
     558                // Return focus to the theme div
     559                if ( themes.focusedTheme ) {
     560                    themes.focusedTheme.focus();
     561                }
     562            });
     563        }
     564    },
     565
     566    // Handles .disabled classes for next/previous buttons
     567    navigation: function() {
     568
     569        // Disable Left/Right when at the start or end of the collection
     570        if ( this.model.cid === this.model.collection.at(0).cid ) {
     571            this.$el.find( '.left' ).addClass( 'disabled' );
     572        }
     573        if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
     574            this.$el.find( '.right' ).addClass( 'disabled' );
     575        }
     576    },
     577
     578    // Performs the actions to effectively close
     579    // the theme details overlay
     580    closeOverlay: function() {
     581        $( 'body' ).removeClass( 'theme-overlay-open' );
     582        this.remove();
     583        this.unbind();
     584        this.trigger( 'theme:collapse' );
     585    },
     586
     587    // Confirmation dialoge for deleting a theme
     588    deleteTheme: function() {
     589        return confirm( themes.data.settings.confirmDelete );
     590    },
     591
     592    nextTheme: function() {
     593        var self = this;
     594        self.trigger( 'theme:next', self.model.cid );
     595    },
     596
     597    previousTheme: function() {
     598        var self = this;
     599        self.trigger( 'theme:previous', self.model.cid );
     600    },
     601
     602    // Checks if the theme screenshot is the old 300px width version
     603    // and adds a corresponding class if it's true
     604    screenshotCheck: function( el ) {
     605        var screenshot, image;
     606
     607        screenshot = el.find( '.screenshot img' );
     608        image = new Image();
     609        image.src = screenshot.attr( 'src' );
     610
     611        // Width check
     612        if ( image.width && image.width <= 300 ) {
     613            el.addClass( 'small-screenshot' );
     614        }
     615    }
     616});
     617
     618// Theme Preview view
     619// Set ups a modal overlay with the expanded theme data
     620themes.view.Preview = wp.Backbone.View.extend({
     621
     622    className: 'wp-full-overlay expanded',
     623    el: '#theme-installer',
     624
     625    events: {
     626        'click .close-full-overlay': 'close',
     627        'click .collapse-sidebar': 'collapse'
     628    },
     629
     630    // The HTML template for the theme preview
     631    html: themes.template( 'theme-preview' ),
     632
     633    render: function() {
     634        var data = this.model.toJSON();
     635        this.$el.html( this.html( data ) );
     636
     637        themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.get( 'id' ) ), { replace: true } );
     638
     639        this.$el.fadeIn( 200, function() {
     640            $( 'body' ).addClass( 'theme-installer-active full-overlay-active' );
     641        });
     642    },
     643
     644    close: function() {
     645        this.$el.fadeOut( 200, function() {
     646            $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
     647        });
     648
     649        themes.router.navigate( themes.router.baseUrl( '' ) );
     650        return false;
     651    },
     652
     653    collapse: function() {
     654        this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
     655        return false;
     656    }
     657});
     658
     659// Controls the rendering of div.themes,
     660// a wrapper that will hold all the theme elements
     661themes.view.Themes = wp.Backbone.View.extend({
     662
     663    className: 'themes',
     664    $overlay: $( 'div.theme-overlay' ),
     665
     666    // Number to keep track of scroll position
     667    // while in theme-overlay mode
     668    index: 0,
     669
     670    // The theme count element
     671    count: $( '.theme-count' ),
     672
     673    initialize: function( options ) {
     674        var self = this;
     675
     676        // Set up parent
     677        this.parent = options.parent;
     678
     679        // Set current view to [grid]
     680        this.setView( 'grid' );
     681
     682        // Move the active theme to the beginning of the collection
     683        self.currentTheme();
     684
     685        // When the collection is updated by user input...
     686        this.listenTo( self.collection, 'update', function() {
     687            self.parent.page = 0;
     688            self.currentTheme();
     689            self.render( this );
     690        });
     691
     692        // Update theme count to full result set when available.
     693        this.listenTo( self.collection, 'query:success', function( count ) {
     694            if ( _.isNumber( count ) ) {
     695                self.count.text( count );
     696            } else {
     697                self.count.text( self.collection.length );
     698            }
     699        });
     700
     701        this.listenTo( this.parent, 'theme:scroll', function() {
     702            self.renderThemes( self.parent.page );
     703        });
     704
     705        this.listenTo( this.parent, 'theme:close', function() {
     706            if ( self.overlay ) {
     707                self.overlay.closeOverlay();
     708            }
     709        } );
     710
     711        // Bind keyboard events.
     712        $( 'body' ).on( 'keyup', function( event ) {
     713            if ( ! self.overlay ) {
     714                return;
     715            }
     716
     717            // Pressing the right arrow key fires a theme:next event
     718            if ( event.keyCode === 39 ) {
     719                self.overlay.nextTheme();
     720            }
     721
     722            // Pressing the left arrow key fires a theme:previous event
     723            if ( event.keyCode === 37 ) {
     724                self.overlay.previousTheme();
     725            }
     726
     727            // Pressing the escape key fires a theme:collapse event
     728            if ( event.keyCode === 27 ) {
     729                self.overlay.collapse( event );
     730            }
     731        });
     732    },
     733
     734    // Manages rendering of theme pages
     735    // and keeping theme count in sync
     736    render: function() {
     737        // Clear the DOM, please
     738        this.$el.html( '' );
     739
     740        // If the user doesn't have switch capabilities
     741        // or there is only one theme in the collection
     742        // render the detailed view of the active theme
     743        if ( themes.data.themes.length === 1 ) {
     744
     745            // Constructs the view
     746            this.singleTheme = new themes.view.Details({
     747                model: this.collection.models[0]
     748            });
     749
     750            // Render and apply a 'single-theme' class to our container
     751            this.singleTheme.render();
     752            this.$el.addClass( 'single-theme' );
     753            this.$el.append( this.singleTheme.el );
     754        }
     755
     756        // Generate the themes
     757        // Using page instance
     758        // While checking the collection has items
     759        if ( this.options.collection.size() > 0 ) {
     760            this.renderThemes( this.parent.page );
     761        }
     762
     763        // Display a live theme count for the collection
     764        this.count.text( this.collection.length );
     765    },
     766
     767    // Iterates through each instance of the collection
     768    // and renders each theme module
     769    renderThemes: function( page ) {
     770        var self = this;
     771
     772        self.instance = self.collection.paginate( page );
     773
     774        // If we have no more themes bail
     775        if ( self.instance.size() === 0 ) {
     776            // Fire a no-more-themes event.
     777            this.parent.trigger( 'theme:end' );
     778            return;
     779        }
     780
     781        // Make sure the add-new stays at the end
     782        if ( page >= 1 ) {
     783            $( '.add-new-theme' ).remove();
     784        }
     785
     786        // Loop through the themes and setup each theme view
     787        self.instance.each( function( theme ) {
     788            self.theme = new themes.view.Theme({
     789                model: theme
     790            });
     791
     792            // Render the views...
     793            self.theme.render();
     794            // and append them to div.themes
     795            self.$el.append( self.theme.el );
     796
     797            // Binds to theme:expand to show the modal box
     798            // with the theme details
     799            self.listenTo( self.theme, 'theme:expand', self.expand, self );
     800        });
     801
     802        // 'Add new theme' element shown at the end of the grid
     803        if ( themes.data.settings.canInstall ) {
     804            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>' );
     805        }
     806
     807        this.parent.page++;
     808    },
     809
     810    // Grabs current theme and puts it at the beginning of the collection
     811    currentTheme: function() {
     812        var self = this,
     813            current;
     814
     815        current = self.collection.findWhere({ active: true });
     816
     817        // Move the active theme to the beginning of the collection
     818        if ( current ) {
     819            self.collection.remove( current );
     820            self.collection.add( current, { at:0 } );
     821        }
     822    },
     823
     824    // Sets current view
     825    setView: function( view ) {
     826        return view;
     827    },
     828
     829    // Renders the overlay with the ThemeDetails view
     830    // Uses the current model data
     831    expand: function( id ) {
     832        var self = this;
     833
     834        // Set the current theme model
     835        this.model = self.collection.get( id );
     836
     837        // Trigger a route update for the current model
     838        themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.id ) );
     839
     840        // Sets this.view to 'detail'
     841        this.setView( 'detail' );
     842        $( 'body' ).addClass( 'theme-overlay-open' );
     843
     844        // Set up the theme details view
     845        this.overlay = new themes.view.Details({
     846            model: self.model
     847        });
     848
     849        this.overlay.render();
     850        this.$overlay.html( this.overlay.el );
     851
     852        // Bind to theme:next and theme:previous
     853        // triggered by the arrow keys
     854        //
     855        // Keep track of the current model so we
     856        // can infer an index position
     857        this.listenTo( this.overlay, 'theme:next', function() {
     858            // Renders the next theme on the overlay
     859            self.next( [ self.model.cid ] );
     860
     861        })
     862        .listenTo( this.overlay, 'theme:previous', function() {
     863            // Renders the previous theme on the overlay
     864            self.previous( [ self.model.cid ] );
     865        });
     866    },
     867
     868    // This method renders the next theme on the overlay modal
     869    // based on the current position in the collection
     870    // @params [model cid]
     871    next: function( args ) {
     872        var self = this,
     873            model, nextModel;
     874
     875        // Get the current theme
     876        model = self.collection.get( args[0] );
     877        // Find the next model within the collection
     878        nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
     879
     880        // Sanity check which also serves as a boundary test
     881        if ( nextModel !== undefined ) {
     882
     883            // We have a new theme...
     884            // Close the overlay
     885            this.overlay.closeOverlay();
     886
     887            // Trigger a route update for the current model
     888            self.theme.trigger( 'theme:expand', nextModel.cid );
     889
     890        }
     891    },
     892
     893    // This method renders the previous theme on the overlay modal
     894    // based on the current position in the collection
     895    // @params [model cid]
     896    previous: function( args ) {
     897        var self = this,
     898            model, previousModel;
     899
     900        // Get the current theme
     901        model = self.collection.get( args[0] );
     902        // Find the previous model within the collection
     903        previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
     904
     905        if ( previousModel !== undefined ) {
     906
     907            // We have a new theme...
     908            // Close the overlay
     909            this.overlay.closeOverlay();
     910
     911            // Trigger a route update for the current model
     912            self.theme.trigger( 'theme:expand', previousModel.cid );
     913
     914        }
     915    }
     916});
     917
     918// Search input view controller.
     919themes.view.Search = wp.Backbone.View.extend({
     920
     921    tagName: 'input',
     922    className: 'theme-search',
     923    id: 'theme-search-input',
     924    searching: false,
     925
     926    attributes: {
     927        placeholder: l10n.searchPlaceholder,
     928        type: 'search'
     929    },
     930
     931    events: {
     932        'input':  'search',
     933        'keyup':  'search',
     934        'change': 'search',
     935        'search': 'search',
     936        'blur':   'pushState'
     937    },
     938
     939    initialize: function( options ) {
     940
     941        this.parent = options.parent;
     942
     943        this.listenTo( this.parent, 'theme:close', function() {
     944            this.searching = false;
     945        } );
     946
     947    },
     948
     949    // Runs a search on the theme collection.
     950    search: function( event ) {
     951        var options = {};
     952
     953        // Clear on escape.
     954        if ( event.type === 'keyup' && event.which === 27 ) {
     955            event.target.value = '';
     956        }
     957
     958        // Lose input focus when pressing enter
     959        if ( event.which === 13 ) {
     960            this.$el.trigger( 'blur' );
     961        }
     962
     963        this.collection.doSearch( event.target.value );
     964
     965        // if search is initiated and key is not return
     966        if ( this.searching && event.which !== 13 ) {
     967            options.replace = true;
     968        } else {
     969            this.searching = true;
     970        }
     971
     972        // Update the URL hash
     973        if ( event.target.value ) {
     974            themes.router.navigate( themes.router.baseUrl( '?search=' + event.target.value ), options );
     975        } else {
     976            themes.router.navigate( themes.router.baseUrl( '' ) );
     977        }
     978    },
     979
     980    pushState: function( event ) {
     981        var url = themes.router.baseUrl( '' );
     982
     983        if ( event.target.value ) {
     984            url = themes.router.baseUrl( '?search=' + event.target.value );
     985        }
     986
     987        this.searching = false;
     988        themes.router.navigate( url );
     989
     990    }
     991});
     992
     993// Sets up the routes events for relevant url queries
     994// Listens to [theme] and [search] params
     995themes.Router = Backbone.Router.extend({
     996
     997    routes: {
     998        'themes.php?theme=:slug': 'theme',
     999        'themes.php?search=:query': 'search',
     1000        'themes.php?s=:query': 'search',
     1001        'themes.php': 'themes',
     1002        '': 'themes'
     1003    },
     1004
     1005    baseUrl: function( url ) {
     1006        return 'themes.php' + url;
     1007    },
     1008
     1009    search: function( query ) {
     1010        $( '.theme-search' ).val( query );
     1011    },
     1012
     1013    themes: function() {
     1014        $( '.theme-search' ).val( '' );
     1015    }
     1016
     1017});
     1018
     1019// Execute and setup the application
     1020themes.Run = {
     1021    init: function() {
     1022        // Initializes the blog's theme library view
     1023        // Create a new collection with data
     1024        this.themes = new themes.Collection( themes.data.themes );
     1025
     1026        // Set up the view
     1027        this.view = new themes.view.Appearance({
     1028            collection: this.themes
     1029        });
     1030
     1031        this.render();
     1032    },
     1033
     1034    render: function() {
     1035
     1036        // Render results
     1037        this.view.render();
     1038        this.routes();
     1039
     1040        Backbone.history.start({
     1041            root: themes.data.settings.adminUrl,
     1042            pushState: true,
     1043            hashChange: false
     1044        });
     1045    },
     1046
     1047    routes: function() {
     1048        var self = this;
     1049        // Bind to our global thx object
     1050        // so that the object is available to sub-views
     1051        themes.router = new themes.Router();
     1052
     1053        // Handles theme details route event
     1054        themes.router.on( 'route:theme', function( slug ) {
     1055            self.view.view.expand( slug );
     1056        });
     1057
     1058        themes.router.on( 'route:themes', function() {
     1059            self.themes.doSearch( '' );
     1060            self.view.trigger( 'theme:close' );
     1061        });
     1062
     1063        // Handles search route event
     1064        themes.router.on( 'route:search', function( query ) {
     1065            self.view.trigger( 'theme:close' );
     1066            self.themes.doSearch( query );
     1067        });
     1068
     1069        this.extraRoutes();
     1070    },
     1071
     1072    extraRoutes: function() {
     1073        return false;
     1074    }
     1075};
     1076
     1077// Extend the main Search view
     1078themes.view.InstallerSearch =  themes.view.Search.extend({
     1079
     1080    events: {
     1081        'keyup': 'search'
     1082    },
     1083
     1084    // Handles Ajax request for searching through themes in public repo
     1085    search: function( event ) {
     1086
     1087        // Tabbing or reverse tabbing into the search input shouldn't trigger a search
     1088        if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
     1089            return;
     1090        }
     1091
     1092        this.collection = this.options.parent.view.collection;
     1093
     1094        // Clear on escape.
     1095        if ( event.type === 'keyup' && event.which === 27 ) {
     1096            event.target.value = '';
     1097        }
     1098
     1099        _.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value );
     1100    },
     1101
     1102    doSearch: _.debounce( function( value ) {
     1103        var request = {},
     1104            self = this;
     1105
     1106        request.search = value;
     1107
     1108        // Intercept an [author] search.
     1109        //
     1110        // If input value starts with `author:` send a request
     1111        // for `author` instead of a regular `search`
     1112        if ( value.substring( 0, 7 ) === 'author:' ) {
     1113            request.search = '';
     1114            request.author = value.slice( 7 );
     1115        }
     1116
     1117        // Intercept a [tag] search.
     1118        //
     1119        // If input value starts with `tag:` send a request
     1120        // for `tag` instead of a regular `search`
     1121        if ( value.substring( 0, 4 ) === 'tag:' ) {
     1122            request.search = '';
     1123            request.tag = [ value.slice( 4 ) ];
     1124        }
     1125
     1126        // Get the themes by sending Ajax POST request to api.wordpress.org/themes
     1127        // or searching the local cache
     1128        this.collection.query( request );
     1129    }, 300 ),
     1130});
     1131
     1132themes.view.Installer = themes.view.Appearance.extend({
     1133
     1134    el: '#wpbody-content .wrap',
     1135
     1136    // Register events for sorting and filters in theme-navigation
     1137    events: {
     1138        'click .theme-section': 'onSort',
     1139        'click .theme-filter': 'onFilter',
     1140        'click .more-filters': 'moreFilters',
     1141        'click [type="checkbox"]': 'addFilter'
    10591142    },
    10601143
     
    10631146        var self = this;
    10641147
    1065         // @todo Cache the collection after fetching based on the section
    10661148        this.collection = new themes.Collection();
     1149
     1150        // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
     1151        this.listenTo( this, 'theme:end', function() {
     1152            self.collection.currentQuery.page++;
     1153            _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
     1154            self.collection.query( self.collection.currentQuery.request );
     1155        });
     1156
     1157        this.listenTo( this.collection, 'query:success', function() {
     1158            $( 'body' ).removeClass( 'loading-themes' );
     1159            $( '.theme-browser' ).find( 'div.error' ).remove();
     1160        });
     1161
     1162        this.listenTo( this.collection, 'query:fail', function() {
     1163            $( '.theme-browser' ).find( 'div.error' ).remove();
     1164            $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
     1165        });
    10671166
    10681167        // Create a new collection with the proper theme data
    10691168        // for each section
    1070         this.apiCall({ browse: section }).done( function( data ) {
    1071             // Update the collection with the queried data
    1072             self.collection.reset( data.themes );
    1073             // Trigger a collection refresh event to render the views
    1074             self.collection.trigger( 'update' );
    1075 
    1076             // Un-spin it
    1077             $( 'body' ).removeClass( 'loading-themes' );
    1078             $( '.theme-browser' ).find( 'div.error' ).remove();
    1079         });
     1169        this.collection.query( { browse: section } );
    10801170
    10811171        if ( this.view ) {
     
    11541244        // Construct the filter request
    11551245        // using the default values
    1156 
    1157         // @todo Cache the collection after fetching based on the filter
    11581246        filter = _.union( filter, this.filtersChecked() );
    11591247        request = { tag: [ filter ] };
    11601248
    1161         // Send Ajax POST request to api.wordpress.org/themes
    1162         this.apiCall( request ).done( function( data ) {
    1163                 // Update the collection with the queried data
    1164                 self.collection.reset( data.themes );
    1165                 // Trigger a collection refresh event to render the views
    1166                 self.collection.trigger( 'update' );
    1167 
    1168                 // Un-spin it
    1169                 $( 'body' ).removeClass( 'loading-themes' );
    1170                 $( '.theme-browser' ).find( 'div.error' ).remove();
    1171         }).fail( function() {
    1172                 $( '.theme-browser' ).find( 'div.error' ).remove();
    1173                 $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
    1174         });
    1175 
    1176         return false;
     1249        // Get the themes by sending Ajax POST request to api.wordpress.org/themes
     1250        // or searching the local cache
     1251        this.collection.query( request );
    11771252    },
    11781253
     
    11831258            request = { tag: tags };
    11841259
    1185         // Send Ajax POST request to api.wordpress.org/themes
    1186         this.apiCall( request ).done( function( data ) {
    1187                 // Update the collection with the queried data
    1188                 self.collection.reset( data.themes );
    1189                 // Trigger a collection refresh event to render the views
    1190                 self.collection.trigger( 'update' );
    1191 
    1192                 // Un-spin it
    1193                 $( 'body' ).removeClass( 'loading-themes' );
    1194                 $( '.theme-browser' ).find( 'div.error' ).remove();
    1195         }).fail( function() {
    1196                 $( '.theme-browser' ).find( 'div.error' ).remove();
    1197                 $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
    1198         });
     1260        // Get the themes by sending Ajax POST request to api.wordpress.org/themes
     1261        // or searching the local cache
     1262        this.collection.query( request );
    11991263    },
    12001264
Note: See TracChangeset for help on using the changeset viewer.