Making WordPress.org

Changeset 1434


Ignore:
Timestamp:
03/20/2015 12:10:03 AM (11 years ago)
Author:
obenland
Message:

WP.org Themes: Consolidate JS handler.

Removes the dependency on the theme-install JS, as well as 4 unnecessary
requests each pageload.

Location:
sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-themes
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-themes/functions.php

    r1416 r1434  
    4141
    4242    if ( ! is_singular( 'page' ) ) {
    43         wp_enqueue_script( 'google-jsapi', '//www.google.com/jsapi', array( 'jquery' ), null );
     43        wp_enqueue_script( 'google-jsapi', '//www.google.com/jsapi', array( 'jquery' ), null, true );
     44        wp_enqueue_script( 'wporg-theme', get_template_directory_uri() . '/js/theme.js', array( 'wp-backbone' ), filemtime( __DIR__ . '/js/theme.js' ), true );
    4445
    45         wp_enqueue_script( 'theme', self_admin_url( 'js/theme.js' ), array( 'wp-backbone' ), false, true );
    46         wp_enqueue_script( 'wporg-theme', get_template_directory_uri() . '/js/theme.js', array( 'theme' ), filemtime( __DIR__ . '/js/theme.js' ), true );
    47 
    48         wp_localize_script( 'theme', '_wpThemeSettings', array(
     46        wp_localize_script( 'wporg-theme', '_wpThemeSettings', array(
    4947            'themes'   => false,
    5048            'query'    => wporg_themes_prepare_themes_for_js(),
    5149            'settings' => array(
    52                 'title'      => __( 'WordPress › %s « Free WordPress Themes', 'wporg-themes' ),
    53                 'isMobile'   => wp_is_mobile(),
    54                 'isInstall'  => true,
    55                 'canInstall' => false,
    56                 'installURI' => null,
    57                 'adminUrl'   => trailingslashit( parse_url( home_url(), PHP_URL_PATH ) ),
     50                'title'        => __( 'WordPress › %s « Free WordPress Themes', 'wporg-themes' ),
     51                'isMobile'     => wp_is_mobile(),
     52                'postsPerPage' => get_option( 'posts_per_page', 15 ),
     53                'path'         => trailingslashit( parse_url( home_url(), PHP_URL_PATH ) ),
    5854            ),
    5955            'l10n' => array(
    60                 'addNew'            => __( 'Add New Theme' ),
    61                 'search'            => __( 'Search Themes' ),
    62                 'searchPlaceholder' => __( 'Search themes...' ), // placeholder (no ellipsis)
    63                 'upload'            => __( 'Upload Theme' ),
    64                 'back'              => __( 'Back' ),
     56                'search'            => __( 'Search Themes', 'wporg-themes' ),
     57                'searchPlaceholder' => __( 'Search themes...', 'wporg-themes' ), // placeholder (no ellipsis)
    6558                'error'             => __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="https://wordpress.org/support/">support forums</a>.' ),
    6659
    6760                // Downloads Graph
    68                 'date'      => __( 'Date' ),
    69                 'downloads' => __( 'Downloads' ),
     61                'date'      => __( 'Date', 'wporg-themes' ),
     62                'downloads' => __( 'Downloads', 'wporg-themes' ),
    7063            ),
    71             'installedThemes' => array(),
    7264        ) );
    7365    }
     66
     67    // No emoji support needed.
     68    remove_action( 'wp_print_styles','print_emoji_styles' );
     69    wp_dequeue_script( 'emoji' );
     70
     71    // No Jetpack styles needed.
     72    add_filter( 'jetpack_implode_frontend_css', '__return_false' );
    7473}
    7574add_action( 'wp_enqueue_scripts', 'wporg_themes_scripts' );
  • sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-themes/js/theme.js

    r1428 r1434  
    1 ( function ( $, wp ) {
    2     google.load("visualization", "1", {packages:["corechart"]});
    3 
    4     wp.themes.utils = {
     1/* global _wpThemeSettings, confirm */
     2window.wp = window.wp || {};
     3
     4( function($) {
     5
     6    // Set up our namespace...
     7    var themes = wp.themes = wp.themes || {},
     8        l10n;
     9
     10    // Store the theme data and settings for organized and quick access
     11    // themes.data.settings, themes.data.themes, themes.data.l10n
     12    themes.data = _wpThemeSettings;
     13    l10n = themes.data.l10n;
     14
     15    // Setup app structure
     16    _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
     17
     18    themes.utils = {
    519        title: function ( item ) {
    620            var title = $( 'title' );
    721
    8             title.html( wp.themes.data.settings.title.replace( '%s', item ) );
     22            title.html( themes.data.settings.title.replace( '%s', item ) );
    923        }
    1024    };
    1125
    12     _.extend( wp.themes.view.Appearance.prototype, {
    13         el: '#themes .theme-browser',
    14         searchContainer: ''
    15     });
    16 
    17     _.extend( wp.themes.view.Themes.prototype, {
    18 
    19         // The theme count element
    20         count: $( '.wp-filter .theme-count' ),
    21 
    22         // Renders the overlay with the ThemeDetails view.
    23         // Uses the current model data.
    24         expand: function( id ) {
    25             var self = this;
    26 
    27             // Set the current theme model
    28             this.model = self.collection.get( id );
    29 
    30             // Trigger a route update for the current model
    31             wp.themes.router.navigate( wp.themes.router.baseUrl( wp.themes.router.themePath + this.model.id ) );
    32             wp.themes.utils.title( this.model.attributes.name );
    33 
    34             // Sets this.view to 'detail'
    35             this.setView( 'detail' );
    36             $( 'body' ).addClass( 'modal-open' );
    37 
    38             // Set up the theme details view
    39             this.overlay = new wp.themes.view.Details({
    40                 model: self.model
    41             });
    42 
    43             this.overlay.render();
    44             this.$overlay.html( this.overlay.el );
    45 
    46             // Bind to theme:next and theme:previous
    47             // triggered by the arrow keys
    48             //
    49             // Keep track of the current model so we
    50             // can infer an index position
    51             this.listenTo( this.overlay, 'theme:next', function() {
    52                 // Renders the next theme on the overlay
    53                 self.next( [ self.model.cid ] );
    54                 $( '.theme-header' ).find( '.right' ).focus();
    55 
    56             })
    57             .listenTo( this.overlay, 'theme:previous', function() {
    58                 // Renders the previous theme on the overlay
    59                 self.previous( [ self.model.cid ] );
    60                 $( '.theme-header' ).find( '.left' ).focus();
    61             });
     26    themes.Model = Backbone.Model.extend({
     27        // Adds attributes to the default data coming through the .org themes api
     28        // Map `id` to `slug` for shared code
     29        initialize: function() {
     30            var description;
     31
     32            // Set the attributes
     33            this.set({
     34                // slug is for installation, id is for existing.
     35                id: this.get( 'slug' ) || this.get( 'id' )
     36            });
     37
     38            // Map `section.description` to `description`
     39            // as the API sometimes returns it differently
     40            if ( this.has( 'sections' ) ) {
     41                description = this.get( 'sections' ).description;
     42                this.set({ description: description });
     43            }
    6244        }
    6345    });
    6446
    65     _.extend( wp.themes.view.Installer.prototype, {
    66         el: '#themes',
    67 
    68         sort: function( sort ) {
    69             var sorter = $( '.filter-links [data-sort="' + sort + '"]'),
     47    // Main view controller for themes.php
     48    // Unifies and renders all available views
     49    themes.view.Appearance = wp.Backbone.View.extend({
     50        el: '#themes .theme-browser',
     51
     52        window: $( window ),
     53
     54        // Pagination instance
     55        page: 0,
     56
     57        // Sets up a throttler for binding to 'scroll'
     58        initialize: function( options ) {
     59            // Scroller checks how far the scroll position is
     60            _.bindAll( this, 'scroller' );
     61
     62            this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
     63            // Bind to the scroll event and throttle
     64            // the results from this.scroller
     65            this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) );
     66        },
     67
     68        // Main render control
     69        render: function() {
     70            // Setup the main theme view
     71            // with the current theme collection
     72            this.view = new themes.view.Themes({
     73                collection: this.collection,
     74                parent: this
     75            });
     76
     77            // Render search form.
     78            this.search();
     79
     80            // Render and append
     81            this.view.render();
     82            this.$el.find( '.themes' ).remove();
     83            this.$el.append( this.view.el ).addClass( 'rendered' );
     84        },
     85
     86        // Defines search element container
     87        searchContainer: '',
     88
     89        // Search input and view
     90        // for current theme collection
     91        search: function() {
     92            var view,
    7093                self = this;
    71             self.clearSearch();
    72 
    73             // Clear filters.
    74             _.each( $( '.filter-group' ).find( ':checkbox' ).filter( ':checked' ), function( item ) {
    75                 $( item ).prop( 'checked', false );
    76                 return self.filtersChecked();
    77             });
    78 
    79             $( '.filter-links li > a, .theme-filter' ).removeClass( this.activeClass );
    80             sorter.addClass( this.activeClass );
    81             wp.themes.utils.title( sorter.text() );
    82 
    83             this.browse( sort );
    84         },
    85 
    86         // Applying filters triggers a tag request.
    87         applyFilters: function( event ) {
    88             var names = [],
    89                 name,
    90                 tags = this.filtersChecked(),
    91                 request = { tag: tags },
    92                 filteringBy = $( '.filtered-by .tags' );
    93 
    94             if ( event ) {
    95                 event.preventDefault();
    96             }
    97 
    98             $( 'body' ).addClass( 'filters-applied' );
    99             $( '.filter-links li > a.current' ).removeClass( 'current' );
    100             filteringBy.empty();
    101 
    102             _.each( tags, function( tag ) {
    103                 name = $( 'label[for="filter-id-' + tag + '"]' ).text();
    104                 names.push( name );
    105                 filteringBy.append( '<span class="tag">' + name + '</span>' );
    106             });
    107 
    108             wp.themes.router.navigate( wp.themes.router.baseUrl( 'tags/' + tags.join( '+' ) ) );
    109             wp.themes.utils.title( names.join( ', ' ) );
    110 
    111             // Get the themes by sending Ajax POST request to api.wordpress.org/themes
    112             // or searching the local cache
    113             this.collection.query( request );
    114         },
    115 
    116         // Toggle the full filters navigation.
    117         moreFilters: function( event ) {
    118             event.preventDefault();
    119 
    120             if ( $( 'body' ).hasClass( 'filters-applied' ) ) {
    121                 return this.backToFilters();
    122             }
    123 
    124             // If the filters section is opened and filters are checked
    125             // run the relevant query collapsing to filtered-by state
    126             if ( $( 'body' ).hasClass( 'show-filters' ) && this.filtersChecked() ) {
    127                 return this.addFilter();
    128             }
    129 
    130             this.clearSearch();
    131 
    132             $( 'body' ).toggleClass( 'show-filters' );
    133         },
    134 
    135         // Get the checked filters.
    136         // @return {array} of tags or false.
    137         filtersChecked: function() {
    138             var items  = $( '.filter-group' ).find( ':checkbox' ).filter( ':checked' ),
    139                 drawer = $( '.filter-drawer' ),
    140                 tags   = [];
    141 
    142             _.each( items, function( item ) {
    143                 tags.push( $( item ).prop( 'value' ) );
    144             });
    145 
    146             // When no filters are checked, restore initial state and return.
    147             if ( 0 === tags.length ) {
    148                 drawer.find( '.apply-filters' ).prop( 'disabled', true ).find( 'span' ).text( '' );
    149                 drawer.find( '.clear-filters' ).hide();
    150                 $( 'body' ).removeClass( 'filters-applied' );
    151                 return false;
    152             }
    153 
    154             drawer.find( '.apply-filters' ).prop( 'disabled', false ).find( 'span' ).text( tags.length );
    155             drawer.find( '.clear-filters' ).css( 'display', 'inline-block' );
    156 
    157             return tags;
    158         },
    159 
     94
     95            view = new this.SearchView({
     96                collection: self.collection,
     97                parent: this
     98            });
     99
     100            // Render and append after screen title
     101            view.render();
     102            this.searchContainer
     103                .append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
     104                .append( view.el );
     105        },
     106
     107        // Checks when the user gets close to the bottom
     108        // of the mage and triggers a theme:scroll event
     109        scroller: function() {
     110            var self = this,
     111                bottom, threshold;
     112
     113            bottom = this.window.scrollTop() + self.window.height();
     114            threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
     115            threshold = Math.round( threshold * 0.9 );
     116
     117            if ( bottom > threshold ) {
     118                this.trigger( 'theme:scroll' );
     119            }
     120        }
    160121    });
    161122
    162     _.extend( wp.themes.view.Theme.prototype, {
     123    // Set up the Collection for our theme data
     124    // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
     125    themes.Collection = Backbone.Collection.extend({
     126        model: themes.Model,
     127
     128        // Search terms
     129        terms: '',
     130
     131        // Local cache array for API queries
     132        queries: [],
     133
     134        // Keep track of current query so we can handle pagination
     135        currentQuery: {
     136            page: 1,
     137            request: {}
     138        },
     139
     140        count: false,
     141
     142        // Static status controller for when we are loading themes.
     143        loadingThemes: false,
     144
     145        // Controls searching on the current theme collection
     146        // and triggers an update event
     147        doSearch: function( value ) {
     148
     149            // Don't do anything if we've already done this search
     150            // Useful because the Search handler fires multiple times per keystroke
     151            if ( this.terms === value ) {
     152                return;
     153            }
     154
     155            // Updates terms with the value passed
     156            this.terms = value;
     157
     158            // If we have terms, run a search...
     159            if ( this.terms.length > 0 ) {
     160                this.search( this.terms );
     161            }
     162
     163            // If search is blank, show all themes
     164            // Useful for resetting the views when you clean the input
     165            if ( this.terms === '' ) {
     166                this.reset( themes.data.themes );
     167            }
     168
     169            // Trigger an 'update' event
     170            this.trigger( 'update' );
     171        },
     172
     173        // Performs a search within the collection
     174        // @uses RegExp
     175        search: function( term ) {
     176            var match, results, haystack, name, description, author;
     177
     178            // Start with a full collection
     179            this.reset( themes.data.themes, { silent: true } );
     180
     181            // Escape the term string for RegExp meta characters
     182            term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
     183
     184            // Consider spaces as word delimiters and match the whole string
     185            // so matching terms can be combined
     186            term = term.replace( / /g, ')(?=.*' );
     187            match = new RegExp( '^(?=.*' + term + ').+', 'i' );
     188
     189            // Find results
     190            // _.filter and .test
     191            results = this.filter( function( data ) {
     192                name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
     193                description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
     194                author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
     195
     196                haystack = _.union( name, data.get( 'id' ), description, author, data.get( 'tags' ) );
     197
     198                if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
     199                    data.set( 'displayAuthor', true );
     200                }
     201
     202                return match.test( haystack );
     203            });
     204
     205            if ( results.length === 0 ) {
     206                this.trigger( 'query:empty' );
     207            } else {
     208                $( 'body' ).removeClass( 'no-results' );
     209            }
     210
     211            this.reset( results );
     212        },
     213
     214        // Paginates the collection with a helper method
     215        // that slices the collection
     216        paginate: function( instance ) {
     217            var collection = this;
     218            instance = instance || 0;
     219
     220            // Themes per instance are set at 15
     221            collection = _( collection.rest( themes.data.settings.postsPerPage * instance ) );
     222            collection = _( collection.first( themes.data.settings.postsPerPage ) );
     223
     224            return collection;
     225        },
     226
     227        // Handles requests for more themes
     228        // and caches results
     229        //
     230        // When we are missing a cache object we fire an apiCall()
     231        // which triggers events of `query:success` or `query:fail`
     232        query: function( request ) {
     233            /**
     234             * @static
     235             * @type Array
     236             */
     237            var queries = this.queries,
     238                self = this,
     239                query, isPaginated, count;
     240
     241            // Store current query request args
     242            // for later use with the event `theme:end`
     243            this.currentQuery.request = request;
     244
     245            // Search the query cache for matches.
     246            query = _.find( queries, function( query ) {
     247                return _.isEqual( query.request, request );
     248            });
     249
     250            // If the request matches the stored currentQuery.request
     251            // it means we have a paginated request.
     252            isPaginated = _.has( request, 'page' );
     253
     254            // Reset the internal api page counter for non paginated queries.
     255            if ( ! isPaginated ) {
     256                this.currentQuery.page = 1;
     257            }
     258
     259            // Otherwise, send a new API call and add it to the cache.
     260            if ( ! query && ! isPaginated ) {
     261                query = this.apiCall( request ).done( function( data ) {
     262
     263                    // Update the collection with the queried data.
     264                    if ( data.themes ) {
     265                        self.reset( data.themes );
     266                        count = data.info.results;
     267                        // Store the results and the query request
     268                        queries.push( { themes: data.themes, request: request, total: count } );
     269                    }
     270
     271                    // Trigger a collection refresh event
     272                    // and a `query:success` event with a `count` argument.
     273                    self.trigger( 'update' );
     274                    self.trigger( 'query:success', count );
     275
     276                    if ( data.themes && data.themes.length === 0 ) {
     277                        self.trigger( 'query:empty' );
     278                    }
     279
     280                }).fail( function() {
     281                    self.trigger( 'query:fail' );
     282                });
     283            } else {
     284                // If it's a paginated request we need to fetch more themes...
     285                if ( isPaginated ) {
     286                    return this.apiCall( request, isPaginated ).done( function( data ) {
     287                        // Add the new themes to the current collection
     288                        // @todo update counter
     289                        self.add( data.themes );
     290                        self.trigger( 'query:success', data.info.results );
     291
     292                        // We are done loading themes for now.
     293                        self.loadingThemes = false;
     294
     295                    }).fail( function() {
     296                        self.trigger( 'query:fail' );
     297                    });
     298                }
     299
     300                if ( query.themes.length === 0 ) {
     301                    self.trigger( 'query:empty' );
     302                } else {
     303                    $( 'body' ).removeClass( 'no-results' );
     304                }
     305
     306                // Only trigger an update event since we already have the themes
     307                // on our cached object
     308                if ( _.isNumber( query.total ) ) {
     309                    this.count = query.total;
     310                }
     311
     312                this.reset( query.themes );
     313                if ( ! query.total ) {
     314                    this.count = this.length;
     315                }
     316
     317                this.trigger( 'update' );
     318                this.trigger( 'query:success', this.count );
     319            }
     320        },
     321
     322        // Send request to api.wordpress.org/themes
     323        apiCall: function( request, paginated ) {
     324            return wp.ajax.send( 'query-themes', {
     325                data: {
     326                    // Request data
     327                    request: _.extend({
     328                        per_page: themes.data.settings.postsPerPage,
     329                        fields: {
     330                            description: true,
     331                            tested: true,
     332                            requires: true,
     333                            rating: true,
     334                            downloaded: true,
     335                            downloadLink: true,
     336                            last_updated: true,
     337                            homepage: true,
     338                            num_ratings: true
     339                        }
     340                    }, request)
     341                },
     342
     343                beforeSend: function() {
     344                    if ( ! paginated ) {
     345                        // Spin it
     346                        $( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
     347                    }
     348                }
     349            });
     350        }
     351    });
     352
     353    // This is the view that controls each theme item
     354    // that will be displayed on the screen
     355    themes.view.Theme = wp.Backbone.View.extend({
     356
     357        // Wrap theme data on a div.theme element
     358        className: 'theme',
     359
     360        // Reflects which theme view we have
     361        // 'grid' (default) or 'detail'
     362        state: 'grid',
     363
     364        // The HTML template for each element to be rendered
     365        html: wp.themes.template( 'theme' ),
     366
    163367        events: {
    164368            'click': 'expand',
     
    169373        },
    170374
     375        touchDrag: false,
     376
    171377        render: function() {
    172378            var data = this.model.toJSON();
    173379
    174             data.permalink = wp.themes.data.settings.adminUrl + wp.themes.router.baseUrl( data.slug );
     380            data.permalink = themes.data.settings.path + themes.router.baseUrl( data.slug );
    175381
    176382            // Render themes using the html template
     
    181387        },
    182388
     389        // Adds a class to the currently active theme
     390        // and to the overlay in detailed view mode
     391        activeTheme: function() {
     392            if ( this.model.get( 'active' ) ) {
     393                this.$el.addClass( 'active' );
     394            }
     395        },
     396
     397        // Add class of focus to the theme we are focused on.
     398        addFocus: function() {
     399            var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
     400
     401            $('.theme.focus').removeClass('focus');
     402            $themeToFocus.addClass('focus');
     403        },
     404
    183405        // Single theme overlay screen
    184406        // It's shown when clicking a theme
     
    210432
    211433            // Set focused theme to current element
    212             wp.themes.focusedTheme = this.$el;
     434            themes.focusedTheme = this.$el;
    213435
    214436            this.trigger( 'theme:expand', self.model.cid );
    215437            event.preventDefault();
     438        },
     439
     440        preventExpand: function() {
     441            this.touchDrag = true;
     442        },
     443
     444        preview: function( event ) {
     445            var self = this,
     446                current, preview;
     447
     448            // Bail if the user scrolled on a touch device
     449            if ( this.touchDrag === true ) {
     450                return this.touchDrag = false;
     451            }
     452
     453            // Allow direct link path to installing a theme.
     454            if ( $( event.target ).hasClass( 'button-primary' ) ) {
     455                return;
     456            }
     457
     458            // 'enter' and 'space' keys expand the details view when a theme is :focused
     459            if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
     460                return;
     461            }
     462
     463            // pressing enter while focused on the buttons shouldn't open the preview
     464            if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
     465                return;
     466            }
     467
     468            event.preventDefault();
     469
     470            event = event || window.event;
     471
     472            // Set focus to current theme.
     473            themes.focusedTheme = this.$el;
     474
     475            // Construct a new Preview view.
     476            preview = new themes.view.Preview({
     477                model: this.model
     478            });
     479
     480            // Render the view and append it.
     481            preview.render();
     482            this.setNavButtonsState();
     483
     484            // Hide previous/next navigation if there is only one theme
     485            if ( this.model.collection.length === 1 ) {
     486                preview.$el.addClass( 'no-navigation' );
     487            } else {
     488                preview.$el.removeClass( 'no-navigation' );
     489            }
     490
     491            // Append preview
     492            $( 'div.wrap' ).append( preview.el );
     493
     494            // Listen to our preview object
     495            // for `theme:next` and `theme:previous` events.
     496            this.listenTo( preview, 'theme:next', function() {
     497
     498                // Keep local track of current theme model.
     499                current = self.model;
     500
     501                // If we have ventured away from current model update the current model position.
     502                if ( ! _.isUndefined( self.current ) ) {
     503                    current = self.current;
     504                }
     505
     506                // Get next theme model.
     507                self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
     508
     509                // If we have no more themes, bail.
     510                if ( _.isUndefined( self.current ) ) {
     511                    self.options.parent.parent.trigger( 'theme:end' );
     512                    return self.current = current;
     513                }
     514
     515                preview.model = self.current;
     516
     517                // Render and append.
     518                preview.render();
     519                this.setNavButtonsState();
     520                $( '.next-theme' ).focus();
     521            })
     522                .listenTo( preview, 'theme:previous', function() {
     523
     524                    // Keep track of current theme model.
     525                    current = self.model;
     526
     527                    // Bail early if we are at the beginning of the collection
     528                    if ( self.model.collection.indexOf( self.current ) === 0 ) {
     529                        return;
     530                    }
     531
     532                    // If we have ventured away from current model update the current model position.
     533                    if ( ! _.isUndefined( self.current ) ) {
     534                        current = self.current;
     535                    }
     536
     537                    // Get previous theme model.
     538                    self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
     539
     540                    // If we have no more themes, bail.
     541                    if ( _.isUndefined( self.current ) ) {
     542                        return;
     543                    }
     544
     545                    preview.model = self.current;
     546
     547                    // Render and append.
     548                    preview.render();
     549                    this.setNavButtonsState();
     550                    $( '.previous-theme' ).focus();
     551                });
     552
     553            this.listenTo( preview, 'preview:close', function() {
     554                self.current = self.model;
     555            });
     556        },
     557
     558        // Handles .disabled classes for previous/next buttons in theme installer preview
     559        setNavButtonsState: function() {
     560            var $themeInstaller = $( '.theme-install-overlay' ),
     561                current = _.isUndefined( this.current ) ? this.model : this.current;
     562
     563            // Disable previous at the zero position
     564            if ( 0 === this.model.collection.indexOf( current ) ) {
     565                $themeInstaller.find( '.previous-theme' ).addClass( 'disabled' );
     566            }
     567
     568            // Disable next if the next model is undefined
     569            if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
     570                $themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
     571            }
    216572        }
    217573    });
    218574
    219 //  wp.themes.view.Preview.prototype = wp.themes.view.Details.prototype;
    220 
    221     _.extend( wp.themes.view.Details.prototype, {
     575// Theme Details view
     576// Set ups a modal overlay with the expanded theme data
     577    themes.view.Details = wp.Backbone.View.extend({
     578        // Wrap theme data on a div.theme element
     579        className: 'theme-overlay',
     580
    222581        events: {
    223582            'click': 'collapse',
     
    230589        },
    231590
     591        // The HTML template for the theme overlay
     592        html: themes.template( 'theme-single' ),
     593
    232594        render: function() {
    233595            var data = this.model.toJSON(),
     
    239601            // Make tags click-able and separated by a comma.
    240602            data.tags = _.map( data.tags, function( tag ) {
    241                 return '<a href="' + wp.themes.data.settings.adminUrl + wp.themes.router.baseUrl( 'tags/' + tag.toLowerCase().replace( ' ', '-' ) ) + '">' + tag + '</a>';
     603                return '<a href="' + themes.data.settings.path + themes.router.baseUrl( 'tags/' + tag.toLowerCase().replace( ' ', '-' ) ) + '">' + tag + '</a>';
    242604            }).join( ', ' );
    243605
     
    278640            }
    279641
     642            event = event || window.event;
    280643            event.preventDefault();
    281644
    282             event = event || window.event;
    283 
    284645            // Set focus to current theme.
    285             wp.themes.focusedTheme = this.$el;
     646            themes.focusedTheme = this.$el;
    286647
    287648            // Construct a new Preview view.
    288             preview = new wp.themes.view.Preview({
     649            preview = new themes.view.Preview({
    289650                model: this.model
    290651            });
     
    301662            }
    302663
    303             if ( wp.themes.data.settings.isMobile ) {
     664            if ( themes.data.settings.isMobile ) {
    304665                preview.$el.addClass( 'wp-full-overlay collapsed' );
    305666            } else {
     
    337698                $( '.next-theme' ).focus();
    338699            })
    339                 .listenTo( preview, 'theme:previous', function() {
    340                     // Keep track of current theme model.
    341                     current = self.model;
    342 
    343                     // Bail early if we are at the beginning of the collection
    344                     if ( self.model.collection.indexOf( self.current ) === 0 ) {
    345                         return;
    346                     }
    347 
    348                     // If we have ventured away from current model update the current model position.
    349                     if ( ! _.isUndefined( self.current ) ) {
    350                         current = self.current;
    351                     }
    352 
    353                     // Get previous theme model.
    354                     self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
    355 
    356                     // If we have no more themes, bail.
    357                     if ( _.isUndefined( self.current ) ) {
    358                         return;
    359                     }
    360 
    361                     preview.model = self.current;
    362 
    363                     // Render and append.
    364                     preview.render();
    365                     this.setNavButtonsState();
    366                     $( '.previous-theme' ).focus();
    367                 });
     700            .listenTo( preview, 'theme:previous', function() {
     701                // Keep track of current theme model.
     702                current = self.model;
     703
     704                // Bail early if we are at the beginning of the collection
     705                if ( self.model.collection.indexOf( self.current ) === 0 ) {
     706                    return;
     707                }
     708
     709                // If we have ventured away from current model update the current model position.
     710                if ( ! _.isUndefined( self.current ) ) {
     711                    current = self.current;
     712                }
     713
     714                // Get previous theme model.
     715                self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
     716
     717                // If we have no more themes, bail.
     718                if ( _.isUndefined( self.current ) ) {
     719                    return;
     720                }
     721
     722                preview.model = self.current;
     723
     724                // Render and append.
     725                preview.render();
     726                this.setNavButtonsState();
     727                $( '.previous-theme' ).focus();
     728            });
    368729
    369730            this.listenTo( preview, 'preview:close', function() {
     
    388749        },
    389750
    390         screenshotCheck: function( el ) {
    391             var image = new Image();
    392             image.src = el.find( '.screenshot img' ).attr( 'src' );
     751        // Adds a class to the currently active theme
     752        // and to the overlay in detailed view mode
     753        activeTheme: function() {
     754            // Check the model has the active property
     755            this.$el.toggleClass( 'active', this.model.get( 'active' ) );
     756        },
     757
     758        // Keeps :focus within the theme details elements.
     759        containFocus: function( $el ) {
     760            var ev = window.event,
     761                $target;
     762
     763            // On first load of the modal, move focus to the primary action.
     764            if ( typeof ev === 'undefined' || 1 === $( ev.target ).closest( '.theme' ).length ) {
     765                _.delay( function() {
     766                    $( '.theme-wrap a.button-primary:visible' ).focus();
     767                }, 500 );
     768            }
     769
     770            $el.on( 'keydown.wp-themes', function( event ) {
     771
     772                // Tab key
     773                if ( event.which === 9 ) {
     774                    $target = $( event.target );
     775
     776                    // Keep focus within the overlay by making the last link on theme actions
     777                    // switch focus to button.left on tabbing and vice versa
     778                    if ( $target.is( 'button.close' ) && event.shiftKey ) {
     779                        $el.find( '.theme-tags a:last-child' ).focus();
     780                        event.preventDefault();
     781                    } else if ( $target.is( '.theme-tags a:last-child' ) ) {
     782                        $el.find( 'button.close' ).focus();
     783                        event.preventDefault();
     784                    }
     785                }
     786            });
     787        },
     788
     789        // Single theme overlay screen
     790        // It's shown when clicking a theme
     791        collapse: function( event ) {
     792            var self = this,
     793                scroll;
     794
     795            event = event || window.event;
     796
     797            // Prevent collapsing detailed view when there is only one theme available
     798            if ( themes.data.themes.length === 1 ) {
     799                return;
     800            }
     801
     802            // Detect if the click is inside the overlay
     803            // and don't close it unless the target was
     804            // the div.back button
     805            if ( $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
     806
     807                // Add a temporary closing class while overlay fades out
     808                $( 'body' ).addClass( 'closing-overlay' );
     809
     810                // With a quick fade out animation
     811                this.$el.fadeOut( 1, function() {
     812                    // Clicking outside the modal box closes the overlay
     813                    $( 'body' ).removeClass( 'closing-overlay' );
     814                    // Handle event cleanup
     815                    self.closeOverlay();
     816
     817                    // Get scroll position to avoid jumping to the top
     818                    scroll = document.body.scrollTop;
     819
     820                    // Restore scroll position
     821                    document.body.scrollTop = scroll;
     822
     823                    // Return focus to the theme div
     824                    if ( themes.focusedTheme ) {
     825                        themes.focusedTheme.focus();
     826                    }
     827                });
     828            }
    393829        },
    394830
     
    439875        },
    440876
    441         // Keeps :focus within the theme details elements.
    442         containFocus: function( $el ) {
    443             var ev = window.event,
    444                 $target;
    445 
    446             // On first load of the modal, move focus to the primary action.
    447             if ( typeof ev === 'undefined' || 1 === $( ev.target ).closest( '.theme' ).length ) {
    448                 _.delay( function() {
    449                     $( '.theme-wrap a.button-primary:visible' ).focus();
    450                 }, 500 );
    451             }
    452 
    453             $el.on( 'keydown.wp-themes', function( event ) {
    454 
    455                 // Tab key
    456                 if ( event.which === 9 ) {
    457                     $target = $( event.target );
    458 
    459                     // Keep focus within the overlay by making the last link on theme actions
    460                     // switch focus to button.left on tabbing and vice versa
    461                     if ( $target.is( 'button.close' ) && event.shiftKey ) {
    462                         $el.find( '.theme-tags a:last-child' ).focus();
    463                         event.preventDefault();
    464                     } else if ( $target.is( '.theme-tags a:last-child' ) ) {
    465                         $el.find( 'button.close' ).focus();
    466                         event.preventDefault();
    467                     }
    468                 }
    469             });
    470         },
    471 
    472         // Single theme overlay screen
    473         // It's shown when clicking a theme
    474         collapse: function( event ) {
    475             var self = this,
    476                 scroll;
    477 
    478             event = event || window.event;
    479 
    480             // Prevent collapsing detailed view when there is only one theme available
    481             if ( wp.themes.data.themes.length === 1 ) {
    482                 return;
    483             }
    484 
    485             // Detect if the click is inside the overlay
    486             // and don't close it unless the target was
    487             // the div.back button
    488             if ( $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
    489 
    490                 // Add a temporary closing class while overlay fades out
    491                 $( 'body' ).addClass( 'closing-overlay' );
    492 
    493                 // With a quick fade out animation
    494                 this.$el.fadeOut( 1, function() {
    495                     // Clicking outside the modal box closes the overlay
    496                     $( 'body' ).removeClass( 'closing-overlay' );
    497                     // Handle event cleanup
    498                     self.closeOverlay();
    499 
    500                     // Get scroll position to avoid jumping to the top
    501                     scroll = document.body.scrollTop;
    502 
    503                     // Restore scroll position
    504                     document.body.scrollTop = scroll;
    505 
    506                     // Return focus to the theme div
    507                     if ( wp.themes.focusedTheme ) {
    508                         wp.themes.focusedTheme.focus();
    509                     }
    510                 });
     877        // Handles .disabled classes for next/previous buttons
     878        navigation: function() {
     879
     880            // Disable Left/Right when at the start or end of the collection
     881            if ( this.model.cid === this.model.collection.at(0).cid ) {
     882                this.$el.find( '.left' ).addClass( 'disabled' );
     883            }
     884            if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
     885                this.$el.find( '.right' ).addClass( 'disabled' );
    511886            }
    512887        },
     
    524899
    525900            // Clean the url structure
    526             if ( author = wp.themes.Collection.prototype.currentQuery.request.author ) {
    527                 wp.themes.router.navigate( wp.themes.router.baseUrl( 'author/' + author ) );
    528                 wp.themes.utils.title( author );
    529             }
    530             else if ( search = wp.themes.Collection.prototype.currentQuery.request.search ) {
    531                 wp.themes.router.navigate( wp.themes.router.baseUrl( wp.themes.router.searchPath + search ) );
    532                 wp.themes.utils.title( search );
    533             }
    534             else if ( tags = wp.themes.view.Installer.prototype.filtersChecked() ) {
    535                 wp.themes.router.navigate( wp.themes.router.baseUrl( 'tags/' + tags.join( '+' ) ) );
    536                 wp.themes.utils.title( _.each( tags, function( tag, i ) {
     901            if ( author = themes.Collection.prototype.currentQuery.request.author ) {
     902                themes.router.navigate( themes.router.baseUrl( 'author/' + author ) );
     903                themes.utils.title( author );
     904            }
     905            else if ( search = themes.Collection.prototype.currentQuery.request.search ) {
     906                themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + search ) );
     907                themes.utils.title( search );
     908            }
     909            else if ( tags = themes.view.Installer.prototype.filtersChecked() ) {
     910                themes.router.navigate( themes.router.baseUrl( 'tags/' + tags.join( '+' ) ) );
     911                themes.utils.title( _.each( tags, function( tag, i ) {
    537912                    tags[ i ] = $( 'label[for="filter-id-' + tag + '"]' ).text();
    538913                }).join( ', ' ) );
     
    543918                    args   = { trigger: true };
    544919                }
    545                 wp.themes.router.navigate( wp.themes.router.baseUrl( wp.themes.router.browsePath + sorter.data( 'sort' ) ), args );
    546                 wp.themes.utils.title( sorter.text() );
    547             }
     920                themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sorter.data( 'sort' ) ), args );
     921                themes.utils.title( sorter.text() );
     922            }
     923        },
     924
     925        // Confirmation dialog for deleting a theme
     926        deleteTheme: function() {
     927            return confirm( themes.data.settings.confirmDelete );
     928        },
     929
     930        nextTheme: function() {
     931            var self = this;
     932            self.trigger( 'theme:next', self.model.cid );
     933            return false;
     934        },
     935
     936        previousTheme: function() {
     937            var self = this;
     938            self.trigger( 'theme:previous', self.model.cid );
     939            return false;
     940        },
     941
     942        screenshotCheck: function( el ) {
     943            var image = new Image();
     944            image.src = el.find( '.screenshot img' ).attr( 'src' );
    548945        }
    549946    });
    550947
    551     _.extend( wp.themes.view.Preview.prototype, {
     948    // Theme Preview view
     949    // Set ups a modal overlay with the expanded theme data
     950    themes.view.Preview = themes.view.Details.extend({
     951
     952        className: 'wp-full-overlay expanded',
     953        el: '.theme-install-overlay',
     954
     955        events: {
     956            'click .close-full-overlay': 'close',
     957            'click .collapse-sidebar': 'collapse',
     958            'click .previous-theme': 'previousTheme',
     959            'click .next-theme': 'nextTheme',
     960            'keyup': 'keyEvent'
     961        },
     962
     963        // The HTML template for the theme preview
     964        html: themes.template( 'theme-preview' ),
     965
    552966
    553967        render: function() {
     
    556970            this.$el.html( this.html( data ) );
    557971
    558             wp.themes.router.navigate( wp.themes.router.baseUrl( wp.themes.router.themePath + this.model.get( 'id' ) + '/preview' ) );
     972            themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) + '/preview' ) );
    559973
    560974            this.$el.fadeIn( 200, function() {
     
    569983
    570984                // Return focus to the theme div
    571                 if ( wp.themes.focusedTheme ) {
    572                     wp.themes.focusedTheme.focus();
     985                if ( themes.focusedTheme ) {
     986                    themes.focusedTheme.focus();
    573987                }
    574988            });
     
    577991            this.undelegateEvents();
    578992            this.unbind();
    579             wp.themes.router.navigate( wp.themes.router.baseUrl( wp.themes.router.themePath + this.model.get( 'id' ) ) );
     993            themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ) );
     994            return false;
     995        },
     996
     997        collapse: function() {
     998
     999            this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
    5801000            return false;
    5811001        },
     
    6021022    });
    6031023
    604     _.extend( wp.themes.view.InstallerSearch.prototype, {
     1024    // Controls the rendering of div.themes,
     1025    // a wrapper that will hold all the theme elements
     1026    themes.view.Themes = wp.Backbone.View.extend({
     1027
     1028        className: 'themes',
     1029        $overlay: $( 'div.theme-overlay' ),
     1030
     1031        // Number to keep track of scroll position
     1032        // while in theme-overlay mode
     1033        index: 0,
     1034
     1035        // The theme count element
     1036        count: $( '.wp-filter .theme-count' ),
     1037
     1038        initialize: function( options ) {
     1039            var self = this;
     1040
     1041            // Set up parent
     1042            this.parent = options.parent;
     1043
     1044            // Set current view to [grid]
     1045            this.setView( 'grid' );
     1046
     1047            // Move the active theme to the beginning of the collection
     1048            self.currentTheme();
     1049
     1050            // When the collection is updated by user input...
     1051            this.listenTo( self.collection, 'update', function() {
     1052                self.parent.page = 0;
     1053                self.currentTheme();
     1054                self.render( this );
     1055            });
     1056
     1057            // Update theme count to full result set when available.
     1058            this.listenTo( self.collection, 'query:success', function( count ) {
     1059                if ( _.isNumber( count ) ) {
     1060                    self.count.text( count );
     1061                } else {
     1062                    self.count.text( self.collection.length );
     1063                }
     1064            });
     1065
     1066            this.listenTo( self.collection, 'query:empty', function() {
     1067                $( 'body' ).addClass( 'no-results' );
     1068            });
     1069
     1070            this.listenTo( this.parent, 'theme:scroll', function() {
     1071                self.renderThemes( self.parent.page );
     1072            });
     1073
     1074            this.listenTo( this.parent, 'theme:close', function() {
     1075                if ( self.overlay ) {
     1076                    self.overlay.closeOverlay();
     1077                }
     1078            } );
     1079
     1080            // Bind keyboard events.
     1081            $( 'body' ).on( 'keyup', function( event ) {
     1082                if ( ! self.overlay ) {
     1083                    return;
     1084                }
     1085
     1086                // Pressing the right arrow key fires a theme:next event
     1087                if ( event.keyCode === 39 ) {
     1088                    self.overlay.nextTheme();
     1089                }
     1090
     1091                // Pressing the left arrow key fires a theme:previous event
     1092                if ( event.keyCode === 37 ) {
     1093                    self.overlay.previousTheme();
     1094                }
     1095
     1096                // Pressing the escape key fires a theme:collapse event
     1097                if ( event.keyCode === 27 ) {
     1098                    self.overlay.collapse( event );
     1099                }
     1100            });
     1101        },
     1102
     1103        // Manages rendering of theme pages
     1104        // and keeping theme count in sync
     1105        render: function() {
     1106            // Clear the DOM, please
     1107            this.$el.empty();
     1108
     1109            // If the user doesn't have switch capabilities
     1110            // or there is only one theme in the collection
     1111            // render the detailed view of the active theme
     1112            if ( themes.data.themes.length === 1 ) {
     1113
     1114                // Constructs the view
     1115                this.singleTheme = new themes.view.Details({
     1116                    model: this.collection.models[0]
     1117                });
     1118
     1119                // Render and apply a 'single-theme' class to our container
     1120                this.singleTheme.render();
     1121                this.$el.addClass( 'single-theme' );
     1122                this.$el.append( this.singleTheme.el );
     1123            }
     1124
     1125            // Generate the themes
     1126            // Using page instance
     1127            // While checking the collection has items
     1128            if ( this.options.collection.size() > 0 ) {
     1129                this.renderThemes( this.parent.page );
     1130            }
     1131
     1132            // Display a live theme count for the collection
     1133            this.count.text( this.collection.count ? this.collection.count : this.collection.length );
     1134        },
     1135
     1136        // Iterates through each instance of the collection
     1137        // and renders each theme module
     1138        renderThemes: function( page ) {
     1139            var self = this;
     1140
     1141            self.instance = self.collection.paginate( page );
     1142
     1143            // If we have no more themes bail
     1144            if ( self.instance.size() === 0 ) {
     1145                // Fire a no-more-themes event.
     1146                this.parent.trigger( 'theme:end' );
     1147                return;
     1148            }
     1149
     1150            // Make sure the add-new stays at the end
     1151            if ( page >= 1 ) {
     1152                $( '.add-new-theme' ).remove();
     1153            }
     1154
     1155            // Loop through the themes and setup each theme view
     1156            self.instance.each( function( theme ) {
     1157                self.theme = new themes.view.Theme({
     1158                    model: theme,
     1159                    parent: self
     1160                });
     1161
     1162                // Render the views...
     1163                self.theme.render();
     1164                // and append them to div.themes
     1165                self.$el.append( self.theme.el );
     1166
     1167                // Binds to theme:expand to show the modal box
     1168                // with the theme details
     1169                self.listenTo( self.theme, 'theme:expand', self.expand, self );
     1170            });
     1171
     1172            this.parent.page++;
     1173        },
     1174
     1175        // Grabs current theme and puts it at the beginning of the collection
     1176        currentTheme: function() {
     1177            var self = this,
     1178                current;
     1179
     1180            current = self.collection.findWhere({ active: true });
     1181
     1182            // Move the active theme to the beginning of the collection
     1183            if ( current ) {
     1184                self.collection.remove( current );
     1185                self.collection.add( current, { at:0 } );
     1186            }
     1187        },
     1188
     1189        // Sets current view
     1190        setView: function( view ) {
     1191            return view;
     1192        },
     1193
     1194        // Renders the overlay with the ThemeDetails view.
     1195        // Uses the current model data.
     1196        expand: function( id ) {
     1197            var self = this;
     1198
     1199            // Set the current theme model
     1200            this.model = self.collection.get( id );
     1201
     1202            // Trigger a route update for the current model
     1203            themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
     1204            themes.utils.title( this.model.attributes.name );
     1205
     1206            // Sets this.view to 'detail'
     1207            this.setView( 'detail' );
     1208            $( 'body' ).addClass( 'modal-open' );
     1209
     1210            // Set up the theme details view
     1211            this.overlay = new themes.view.Details({
     1212                model: self.model
     1213            });
     1214
     1215            this.overlay.render();
     1216            this.$overlay.html( this.overlay.el );
     1217
     1218            // Bind to theme:next and theme:previous
     1219            // triggered by the arrow keys
     1220            //
     1221            // Keep track of the current model so we
     1222            // can infer an index position
     1223            this.listenTo( this.overlay, 'theme:next', function() {
     1224                // Renders the next theme on the overlay
     1225                self.next( [ self.model.cid ] );
     1226                $( '.theme-header' ).find( '.right' ).focus();
     1227
     1228            })
     1229            .listenTo( this.overlay, 'theme:previous', function() {
     1230                // Renders the previous theme on the overlay
     1231                self.previous( [ self.model.cid ] );
     1232                $( '.theme-header' ).find( '.left' ).focus();
     1233            });
     1234        },
     1235
     1236        // This method renders the next theme on the overlay modal
     1237        // based on the current position in the collection
     1238        // @params [model cid]
     1239        next: function( args ) {
     1240            var self = this,
     1241                model, nextModel;
     1242
     1243            // Get the current theme
     1244            model = self.collection.get( args[0] );
     1245            // Find the next model within the collection
     1246            nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
     1247
     1248            // Sanity check which also serves as a boundary test
     1249            if ( nextModel !== undefined ) {
     1250
     1251                // We have a new theme...
     1252                // Close the overlay
     1253                this.overlay.closeOverlay();
     1254
     1255                // Trigger a route update for the current model
     1256                self.theme.trigger( 'theme:expand', nextModel.cid );
     1257
     1258            }
     1259        },
     1260
     1261        // This method renders the previous theme on the overlay modal
     1262        // based on the current position in the collection
     1263        // @params [model cid]
     1264        previous: function( args ) {
     1265            var self = this,
     1266                model, previousModel;
     1267
     1268            // Get the current theme
     1269            model = self.collection.get( args[0] );
     1270            // Find the previous model within the collection
     1271            previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
     1272
     1273            if ( previousModel !== undefined ) {
     1274
     1275                // We have a new theme...
     1276                // Close the overlay
     1277                this.overlay.closeOverlay();
     1278
     1279                // Trigger a route update for the current model
     1280                self.theme.trigger( 'theme:expand', previousModel.cid );
     1281
     1282            }
     1283        }
     1284    });
     1285
     1286    // Search input view controller.
     1287    themes.view.Search = wp.Backbone.View.extend({
     1288
     1289        tagName: 'input',
     1290        className: 'wp-filter-search',
     1291        id: 'wp-filter-search-input',
     1292        searching: false,
     1293
     1294        attributes: {
     1295            placeholder: l10n.searchPlaceholder,
     1296            type: 'search'
     1297        },
     1298
     1299        events: {
     1300            'input':  'search',
     1301            'keyup':  'search',
     1302            'change': 'search',
     1303            'search': 'search',
     1304            'blur':   'pushState'
     1305        },
     1306
     1307        initialize: function( options ) {
     1308
     1309            this.parent = options.parent;
     1310
     1311            this.listenTo( this.parent, 'theme:close', function() {
     1312                this.searching = false;
     1313            } );
     1314
     1315        },
     1316
     1317        // Runs a search on the theme collection.
     1318        search: function( event ) {
     1319            var options = {};
     1320
     1321            // Clear on escape.
     1322            if ( event.type === 'keyup' && event.which === 27 ) {
     1323                event.target.value = '';
     1324            }
     1325
     1326            // Lose input focus when pressing enter
     1327            if ( event.which === 13 ) {
     1328                this.$el.trigger( 'blur' );
     1329            }
     1330
     1331            this.collection.doSearch( event.target.value );
     1332
     1333            // if search is initiated and key is not return
     1334            if ( this.searching && event.which !== 13 ) {
     1335                options.replace = true;
     1336            } else {
     1337                this.searching = true;
     1338            }
     1339
     1340            // Update the URL hash
     1341            if ( event.target.value ) {
     1342                themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
     1343            } else {
     1344                themes.router.navigate( themes.router.baseUrl( '' ) );
     1345            }
     1346        },
     1347
     1348        pushState: function( event ) {
     1349            var url = themes.router.baseUrl( '' );
     1350
     1351            if ( event.target.value ) {
     1352                url = themes.router.baseUrl( themes.router.searchPath + event.target.value );
     1353            }
     1354
     1355            this.searching = false;
     1356            themes.router.navigate( url );
     1357
     1358        }
     1359    });
     1360
     1361    // Sets up the routes events for relevant url queries
     1362    // Listens to [theme] and [search] params
     1363    themes.Router = Backbone.Router.extend({
     1364
     1365        routes: {
     1366            'themes.php?theme=:slug': 'theme',
     1367            'themes.php?search=:query': 'search',
     1368            'themes.php?s=:query': 'search',
     1369            'themes.php': 'themes',
     1370            '': 'themes'
     1371        },
     1372
     1373        baseUrl: function( url ) {
     1374            return 'themes.php' + url;
     1375        },
     1376
     1377        themePath: '?theme=',
     1378        searchPath: '?search=',
     1379
     1380        search: function( query ) {
     1381            $( '.wp-filter-search' ).val( query );
     1382        },
     1383
     1384        themes: function() {
     1385            $( '.wp-filter-search' ).val( '' );
     1386        },
     1387
     1388        navigate: function() {
     1389            if ( Backbone.history._hasPushState ) {
     1390                Backbone.Router.prototype.navigate.apply( this, arguments );
     1391            }
     1392        }
     1393
     1394    });
     1395
     1396    // Extend the main Search view
     1397    themes.view.InstallerSearch =  themes.view.Search.extend({
     1398
    6051399        events: {
    6061400            'keyup':  'search',
    607             'search': 'search',
     1401            'search': 'search'
     1402        },
     1403
     1404        // Handles Ajax request for searching through themes in public repo
     1405        search: function( event ) {
     1406
     1407            // Tabbing or reverse tabbing into the search input shouldn't trigger a search
     1408            if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
     1409                return;
     1410            }
     1411
     1412            this.collection = this.options.parent.view.collection;
     1413
     1414            // Clear on escape.
     1415            if ( event.type === 'keyup' && event.which === 27 ) {
     1416                event.target.value = '';
     1417            }
     1418
     1419            _.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value );
    6081420        },
    6091421
     
    6111423            var request = {};
    6121424
    613             wp.themes.view.Installer.prototype.clearFilters( new Event( 'click' ) );
     1425            themes.view.Installer.prototype.clearFilters( new Event( 'click' ) );
    6141426
    6151427            request.search = value;
     
    6381450            // Set route
    6391451            if ( value ) {
    640                 wp.themes.utils.title( value );
    641                 wp.themes.router.navigate( wp.themes.router.baseUrl( wp.themes.router.searchPath + value ), { replace: true } );
     1452                themes.utils.title( value );
     1453                themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + value ), { replace: true } );
    6421454            } else {
    6431455                delete request.search;
    6441456                request.browse = 'featured';
    6451457
    646                 wp.themes.utils.title( $( '.filter-links [data-sort="featured"]' ).text() );
    647                 wp.themes.router.navigate( wp.themes.router.baseUrl( wp.themes.router.browsePath + 'featured' ), { replace: true } );
     1458                themes.utils.title( $( '.filter-links [data-sort="featured"]' ).text() );
     1459                themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + 'featured' ), { replace: true } );
    6481460            }
    6491461
     
    6541466    });
    6551467
    656     _.extend( wp.themes.InstallerRouter.prototype, {
     1468    themes.view.Installer = themes.view.Appearance.extend({
     1469        el: '#themes',
     1470
     1471        // Register events for sorting and filters in theme-navigation
     1472        events: {
     1473            'click .filter-links li > a': 'onSort',
     1474            'click .theme-filter': 'onFilter',
     1475            'click .drawer-toggle': 'moreFilters',
     1476            'click .filter-drawer .apply-filters': 'applyFilters',
     1477            'click .filter-group [type="checkbox"]': 'addFilter',
     1478            'click .filter-drawer .clear-filters': 'clearFilters',
     1479            'click .filtered-by': 'backToFilters'
     1480        },
     1481
     1482        activeClass: 'current',
     1483
     1484        // Overwrite search container class to append search
     1485        // in new location
     1486        searchContainer: $( '.wp-filter .search-form' ),
     1487
     1488        // Initial render method
     1489        render: function() {
     1490            var self = this;
     1491
     1492            this.search();
     1493
     1494            this.collection = new themes.Collection();
     1495
     1496            // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
     1497            this.listenTo( this, 'theme:end', function() {
     1498
     1499                // Make sure we are not already loading
     1500                if ( self.collection.loadingThemes ) {
     1501                    return;
     1502                }
     1503
     1504                // Set loadingThemes to true and bump page instance of currentQuery.
     1505                self.collection.loadingThemes = true;
     1506                self.collection.currentQuery.page++;
     1507
     1508                // Use currentQuery.page to build the themes request.
     1509                _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
     1510                self.collection.query( self.collection.currentQuery.request );
     1511            });
     1512
     1513            this.listenTo( this.collection, 'query:success', function() {
     1514                $( 'body' ).removeClass( 'loading-content' );
     1515                $( '.theme-browser' ).find( 'div.error' ).remove();
     1516            });
     1517
     1518            this.listenTo( this.collection, 'query:fail', function() {
     1519                $( 'body' ).removeClass( 'loading-content' );
     1520                $( '.theme-browser' ).find( 'div.error' ).remove();
     1521                $( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p></div>' );
     1522            });
     1523
     1524            if ( this.view ) {
     1525                this.view.remove();
     1526            }
     1527
     1528            // Set ups the view and passes the section argument
     1529            this.view = new themes.view.Themes({
     1530                collection: this.collection,
     1531                parent: this
     1532            });
     1533
     1534            // Reset pagination every time the install view handler is run
     1535            this.page = 0;
     1536
     1537            // Render and append
     1538            this.$el.find( '.themes' ).remove();
     1539            this.view.render();
     1540            this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
     1541        },
     1542
     1543        // Handles all the rendering of the public theme directory
     1544        browse: function( section ) {
     1545            // Create a new collection with the proper theme data
     1546            // for each section
     1547            this.collection.query( { browse: section } );
     1548        },
     1549
     1550        // Sorting navigation
     1551        onSort: function( event ) {
     1552            var $el = $( event.target ),
     1553                sort = $el.data( 'sort' );
     1554
     1555            event.preventDefault();
     1556
     1557            $( 'body' ).removeClass( 'filters-applied show-filters' );
     1558
     1559            // Bail if this is already active
     1560            if ( $el.hasClass( this.activeClass ) ) {
     1561                return;
     1562            }
     1563
     1564            this.sort( sort );
     1565
     1566            // Trigger a router.naviagte update
     1567            themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
     1568        },
     1569
     1570        sort: function( sort ) {
     1571            var sorter = $( '.filter-links [data-sort="' + sort + '"]'),
     1572                self = this;
     1573            self.clearSearch();
     1574
     1575            // Clear filters.
     1576            _.each( $( '.filter-group' ).find( ':checkbox' ).filter( ':checked' ), function( item ) {
     1577                $( item ).prop( 'checked', false );
     1578                return self.filtersChecked();
     1579            });
     1580
     1581            $( '.filter-links li > a, .theme-filter' ).removeClass( this.activeClass );
     1582            sorter.addClass( this.activeClass );
     1583            themes.utils.title( sorter.text() );
     1584
     1585            this.browse( sort );
     1586        },
     1587
     1588        // Filters and Tags
     1589        onFilter: function( event ) {
     1590            var request,
     1591                $el = $( event.target ),
     1592                filter = $el.data( 'filter' );
     1593
     1594            // Bail if this is already active
     1595            if ( $el.hasClass( this.activeClass ) ) {
     1596                return;
     1597            }
     1598
     1599            $( '.filter-links li > a, .theme-section' ).removeClass( this.activeClass );
     1600            $el.addClass( this.activeClass );
     1601
     1602            if ( ! filter ) {
     1603                return;
     1604            }
     1605
     1606            // Construct the filter request
     1607            // using the default values
     1608            filter = _.union( filter, this.filtersChecked() );
     1609            request = { tag: [ filter ] };
     1610
     1611            // Get the themes by sending Ajax POST request to api.wordpress.org/themes
     1612            // or searching the local cache
     1613            this.collection.query( request );
     1614        },
     1615
     1616        // Clicking on a checkbox to add another filter to the request
     1617        addFilter: function() {
     1618            this.filtersChecked();
     1619        },
     1620
     1621        // Applying filters triggers a tag request.
     1622        applyFilters: function( event ) {
     1623            var names = [],
     1624                name,
     1625                tags = this.filtersChecked(),
     1626                request = { tag: tags },
     1627                filteringBy = $( '.filtered-by .tags' );
     1628
     1629            if ( event ) {
     1630                event.preventDefault();
     1631            }
     1632
     1633            $( 'body' ).addClass( 'filters-applied' );
     1634            $( '.filter-links li > a.current' ).removeClass( 'current' );
     1635            filteringBy.empty();
     1636
     1637            _.each( tags, function( tag ) {
     1638                name = $( 'label[for="filter-id-' + tag + '"]' ).text();
     1639                names.push( name );
     1640                filteringBy.append( '<span class="tag">' + name + '</span>' );
     1641            });
     1642
     1643            themes.router.navigate( themes.router.baseUrl( 'tags/' + tags.join( '+' ) ) );
     1644            themes.utils.title( names.join( ', ' ) );
     1645
     1646            // Get the themes by sending Ajax POST request to api.wordpress.org/themes
     1647            // or searching the local cache
     1648            this.collection.query( request );
     1649        },
     1650
     1651        // Get the checked filters.
     1652        // @return {array} of tags or false.
     1653        filtersChecked: function() {
     1654            var items  = $( '.filter-group' ).find( ':checkbox' ).filter( ':checked' ),
     1655                drawer = $( '.filter-drawer' ),
     1656                tags   = [];
     1657
     1658            _.each( items, function( item ) {
     1659                tags.push( $( item ).prop( 'value' ) );
     1660            });
     1661
     1662            // When no filters are checked, restore initial state and return.
     1663            if ( 0 === tags.length ) {
     1664                drawer.find( '.apply-filters' ).prop( 'disabled', true ).find( 'span' ).text( '' );
     1665                drawer.find( '.clear-filters' ).hide();
     1666                $( 'body' ).removeClass( 'filters-applied' );
     1667                return false;
     1668            }
     1669
     1670            drawer.find( '.apply-filters' ).prop( 'disabled', false ).find( 'span' ).text( tags.length );
     1671            drawer.find( '.clear-filters' ).css( 'display', 'inline-block' );
     1672
     1673            return tags;
     1674        },
     1675
     1676        // Toggle the full filters navigation.
     1677        moreFilters: function( event ) {
     1678            event.preventDefault();
     1679
     1680            if ( $( 'body' ).hasClass( 'filters-applied' ) ) {
     1681                return this.backToFilters();
     1682            }
     1683
     1684            // If the filters section is opened and filters are checked
     1685            // run the relevant query collapsing to filtered-by state
     1686            if ( $( 'body' ).hasClass( 'show-filters' ) && this.filtersChecked() ) {
     1687                return this.addFilter();
     1688            }
     1689
     1690            this.clearSearch();
     1691
     1692            $( 'body' ).toggleClass( 'show-filters' );
     1693        },
     1694
     1695        // Clears all the checked filters
     1696        // @uses filtersChecked()
     1697        clearFilters: function( event ) {
     1698            var items = $( '.filter-group' ).find( ':checkbox' ),
     1699                self = this;
     1700
     1701            event.preventDefault();
     1702
     1703            _.each( items.filter( ':checked' ), function( item ) {
     1704                $( item ).prop( 'checked', false );
     1705                return self.filtersChecked();
     1706            });
     1707        },
     1708
     1709        backToFilters: function( event ) {
     1710            if ( event ) {
     1711                event.preventDefault();
     1712            }
     1713
     1714            $( 'body' ).removeClass( 'filters-applied' );
     1715        },
     1716
     1717        clearSearch: function() {
     1718            $( '#wp-filter-search-input').val( '' );
     1719        }
     1720    });
     1721
     1722    themes.InstallerRouter = Backbone.Router.extend({
    6571723        routes: {
    6581724            'browse/:sort/'  : 'sort',
     
    6741740        themePath: '',
    6751741        browsePath: 'browse/',
    676         searchPath: 'search/'
     1742        searchPath: 'search/',
     1743
     1744        search: function( query ) {
     1745            $( '.wp-filter-search' ).val( query );
     1746        },
     1747
     1748        navigate: function() {
     1749            if ( Backbone.history._hasPushState ) {
     1750                Backbone.Router.prototype.navigate.apply( this, arguments );
     1751            }
     1752        }
    6771753    });
    6781754
    679     _.extend( wp.themes.RunInstaller, {
     1755    themes.RunInstaller = {
     1756        init: function() {
     1757            // Set up the view
     1758            // Passes the default 'section' as an option
     1759            this.view = new themes.view.Installer({
     1760                section: 'featured',
     1761                SearchView: themes.view.InstallerSearch
     1762            });
     1763
     1764            // Render results
     1765            this.render();
     1766        },
     1767
     1768        render: function() {
     1769
     1770            // Render results
     1771            this.view.render();
     1772            this.routes();
     1773
     1774            Backbone.history.start({
     1775                root: themes.data.settings.path,
     1776                pushState: true,
     1777                hashChange: false
     1778            });
     1779        },
     1780
    6801781        routes: function() {
    6811782            var self = this,
    6821783                request = {};
    6831784
    684             // Bind to our global `wp.themes` object
     1785            // Bind to our global `themes` object
    6851786            // so that the router is available to sub-views
    686             wp.themes.router = new wp.themes.InstallerRouter();
     1787            themes.router = new themes.InstallerRouter();
    6871788
    6881789            // Handles `theme` route event
    6891790            // Queries the API for the passed theme slug
    690             wp.themes.router.on( 'route:preview', function( slug ) {
    691                 self.view.collection.queries.push( wp.themes.data.query );
     1791            themes.router.on( 'route:preview', function( slug ) {
     1792                self.view.collection.queries.push( themes.data.query );
    6921793
    6931794                request.theme = slug;
     
    7011802            // Also handles the root URL triggering a sort request
    7021803            // for `featured`, the default view
    703             wp.themes.router.on( 'route:sort', function( sort ) {
    704                 self.view.collection.queries.push( wp.themes.data.query );
     1804            themes.router.on( 'route:sort', function( sort ) {
     1805                self.view.collection.queries.push( themes.data.query );
    7051806
    7061807                if ( ! sort ) {
     
    7121813
    7131814            // The `search` route event. The router populates the input field.
    714             wp.themes.router.on( 'route:search', function() {
    715                 self.view.collection.queries.push( wp.themes.data.query );
     1815            themes.router.on( 'route:search', function() {
     1816                self.view.collection.queries.push( themes.data.query );
    7161817
    7171818                $( '.wp-filter-search' ).focus().trigger( 'keyup' );
    7181819            });
    7191820
    720             wp.themes.router.on( 'route:tag', function( tag ) {
    721                 self.view.collection.queries.push( wp.themes.data.query );
     1821            themes.router.on( 'route:tag', function( tag ) {
     1822                self.view.collection.queries.push( themes.data.query );
    7221823
    7231824                _.each( tag.split( '+' ), function( tag ) {
     
    7281829            });
    7291830
    730             wp.themes.router.on( 'route:author', function( author ) {
    731                 self.view.collection.queries.push( wp.themes.data.query );
     1831            themes.router.on( 'route:author', function( author ) {
     1832                self.view.collection.queries.push( themes.data.query );
    7321833
    7331834                request.author = author;
    7341835                self.view.collection.query( request );
    735                 wp.themes.utils.title( author );
    736             });
    737 
    738             this.extraRoutes();
     1836                themes.utils.title( author );
     1837            });
    7391838        }
     1839    };
     1840
     1841    // Ready...
     1842    $( function() {
     1843        themes.RunInstaller.init();
    7401844    });
    7411845
    742 }( jQuery, wp ) );
     1846})( jQuery );
     1847
     1848( function( google ) {
     1849    google.load("visualization", "1", {packages:["corechart"]});
     1850})( google );
Note: See TracChangeset for help on using the changeset viewer.