Changeset 1434
- Timestamp:
- 03/20/2015 12:10:03 AM (11 years ago)
- Location:
- sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-themes
- Files:
-
- 2 edited
-
functions.php (modified) (1 diff)
-
js/theme.js (modified) (24 diffs)
Legend:
- Unmodified
- Added
- Removed
-
sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-themes/functions.php
r1416 r1434 41 41 42 42 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 ); 44 45 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( 49 47 'themes' => false, 50 48 'query' => wporg_themes_prepare_themes_for_js(), 51 49 '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 ) ), 58 54 ), 59 55 '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) 65 58 'error' => __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the <a href="https://wordpress.org/support/">support forums</a>.' ), 66 59 67 60 // Downloads Graph 68 'date' => __( 'Date' ),69 'downloads' => __( 'Downloads' ),61 'date' => __( 'Date', 'wporg-themes' ), 62 'downloads' => __( 'Downloads', 'wporg-themes' ), 70 63 ), 71 'installedThemes' => array(),72 64 ) ); 73 65 } 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' ); 74 73 } 75 74 add_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 */ 2 window.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 = { 5 19 title: function ( item ) { 6 20 var title = $( 'title' ); 7 21 8 title.html( wp.themes.data.settings.title.replace( '%s', item ) );22 title.html( themes.data.settings.title.replace( '%s', item ) ); 9 23 } 10 24 }; 11 25 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 } 62 44 } 63 45 }); 64 46 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, 70 93 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 } 160 121 }); 161 122 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 163 367 events: { 164 368 'click': 'expand', … … 169 373 }, 170 374 375 touchDrag: false, 376 171 377 render: function() { 172 378 var data = this.model.toJSON(); 173 379 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 ); 175 381 176 382 // Render themes using the html template … … 181 387 }, 182 388 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 183 405 // Single theme overlay screen 184 406 // It's shown when clicking a theme … … 210 432 211 433 // Set focused theme to current element 212 wp.themes.focusedTheme = this.$el;434 themes.focusedTheme = this.$el; 213 435 214 436 this.trigger( 'theme:expand', self.model.cid ); 215 437 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 } 216 572 } 217 573 }); 218 574 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 222 581 events: { 223 582 'click': 'collapse', … … 230 589 }, 231 590 591 // The HTML template for the theme overlay 592 html: themes.template( 'theme-single' ), 593 232 594 render: function() { 233 595 var data = this.model.toJSON(), … … 239 601 // Make tags click-able and separated by a comma. 240 602 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>'; 242 604 }).join( ', ' ); 243 605 … … 278 640 } 279 641 642 event = event || window.event; 280 643 event.preventDefault(); 281 644 282 event = event || window.event;283 284 645 // Set focus to current theme. 285 wp.themes.focusedTheme = this.$el;646 themes.focusedTheme = this.$el; 286 647 287 648 // Construct a new Preview view. 288 preview = new wp.themes.view.Preview({649 preview = new themes.view.Preview({ 289 650 model: this.model 290 651 }); … … 301 662 } 302 663 303 if ( wp.themes.data.settings.isMobile ) {664 if ( themes.data.settings.isMobile ) { 304 665 preview.$el.addClass( 'wp-full-overlay collapsed' ); 305 666 } else { … … 337 698 $( '.next-theme' ).focus(); 338 699 }) 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 collection344 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 }); 368 729 369 730 this.listenTo( preview, 'preview:close', function() { … … 388 749 }, 389 750 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 } 393 829 }, 394 830 … … 439 875 }, 440 876 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' ); 511 886 } 512 887 }, … … 524 899 525 900 // 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 ) { 537 912 tags[ i ] = $( 'label[for="filter-id-' + tag + '"]' ).text(); 538 913 }).join( ', ' ) ); … … 543 918 args = { trigger: true }; 544 919 } 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' ); 548 945 } 549 946 }); 550 947 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 552 966 553 967 render: function() { … … 556 970 this.$el.html( this.html( data ) ); 557 971 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' ) ); 559 973 560 974 this.$el.fadeIn( 200, function() { … … 569 983 570 984 // Return focus to the theme div 571 if ( wp.themes.focusedTheme ) {572 wp.themes.focusedTheme.focus();985 if ( themes.focusedTheme ) { 986 themes.focusedTheme.focus(); 573 987 } 574 988 }); … … 577 991 this.undelegateEvents(); 578 992 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' ); 580 1000 return false; 581 1001 }, … … 602 1022 }); 603 1023 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 605 1399 events: { 606 1400 '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 ); 608 1420 }, 609 1421 … … 611 1423 var request = {}; 612 1424 613 wp.themes.view.Installer.prototype.clearFilters( new Event( 'click' ) );1425 themes.view.Installer.prototype.clearFilters( new Event( 'click' ) ); 614 1426 615 1427 request.search = value; … … 638 1450 // Set route 639 1451 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 } ); 642 1454 } else { 643 1455 delete request.search; 644 1456 request.browse = 'featured'; 645 1457 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 } ); 648 1460 } 649 1461 … … 654 1466 }); 655 1467 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({ 657 1723 routes: { 658 1724 'browse/:sort/' : 'sort', … … 674 1740 themePath: '', 675 1741 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 } 677 1753 }); 678 1754 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 680 1781 routes: function() { 681 1782 var self = this, 682 1783 request = {}; 683 1784 684 // Bind to our global ` wp.themes` object1785 // Bind to our global `themes` object 685 1786 // so that the router is available to sub-views 686 wp.themes.router = new wp.themes.InstallerRouter();1787 themes.router = new themes.InstallerRouter(); 687 1788 688 1789 // Handles `theme` route event 689 1790 // 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 ); 692 1793 693 1794 request.theme = slug; … … 701 1802 // Also handles the root URL triggering a sort request 702 1803 // 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 ); 705 1806 706 1807 if ( ! sort ) { … … 712 1813 713 1814 // 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 ); 716 1817 717 1818 $( '.wp-filter-search' ).focus().trigger( 'keyup' ); 718 1819 }); 719 1820 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 ); 722 1823 723 1824 _.each( tag.split( '+' ), function( tag ) { … … 728 1829 }); 729 1830 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 ); 732 1833 733 1834 request.author = author; 734 1835 self.view.collection.query( request ); 735 wp.themes.utils.title( author ); 736 }); 737 738 this.extraRoutes(); 1836 themes.utils.title( author ); 1837 }); 739 1838 } 1839 }; 1840 1841 // Ready... 1842 $( function() { 1843 themes.RunInstaller.init(); 740 1844 }); 741 1845 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.