Making WordPress.org

Changeset 1434


Ignore:
Timestamp:
03/20/2015 12:10:03 AM (12 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.