[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-admin/js/ -> theme.js (source)

   1  /**
   2   * @output wp-admin/js/theme.js
   3   */
   4  
   5  /* global _wpThemeSettings, confirm, tb_position */
   6  window.wp = window.wp || {};
   7  
   8  ( function($) {
   9  
  10  // Set up our namespace...
  11  var themes, l10n;
  12  themes = wp.themes = wp.themes || {};
  13  
  14  // Store the theme data and settings for organized and quick access.
  15  // themes.data.settings, themes.data.themes, themes.data.l10n.
  16  themes.data = _wpThemeSettings;
  17  l10n = themes.data.l10n;
  18  
  19  // Shortcut for isInstall check.
  20  themes.isInstall = !! themes.data.settings.isInstall;
  21  
  22  // Setup app structure.
  23  _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
  24  
  25  themes.Model = Backbone.Model.extend({
  26      // Adds attributes to the default data coming through the .org themes api.
  27      // Map `id` to `slug` for shared code.
  28      initialize: function() {
  29          var description;
  30  
  31          // If theme is already installed, set an attribute.
  32          if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
  33              this.set({ installed: true });
  34          }
  35  
  36          // Set the attributes.
  37          this.set({
  38              // `slug` is for installation, `id` is for existing.
  39              id: this.get( 'slug' ) || this.get( 'id' )
  40          });
  41  
  42          // Map `section.description` to `description`
  43          // as the API sometimes returns it differently.
  44          if ( this.has( 'sections' ) ) {
  45              description = this.get( 'sections' ).description;
  46              this.set({ description: description });
  47          }
  48      }
  49  });
  50  
  51  // Main view controller for themes.php.
  52  // Unifies and renders all available views.
  53  themes.view.Appearance = wp.Backbone.View.extend({
  54  
  55      el: '#wpbody-content .wrap .theme-browser',
  56  
  57      window: $( window ),
  58      // Pagination instance.
  59      page: 0,
  60  
  61      // Sets up a throttler for binding to 'scroll'.
  62      initialize: function( options ) {
  63          // Scroller checks how far the scroll position is.
  64          _.bindAll( this, 'scroller' );
  65  
  66          this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
  67          // Bind to the scroll event and throttle
  68          // the results from this.scroller.
  69          this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) );
  70      },
  71  
  72      // Main render control.
  73      render: function() {
  74          // Setup the main theme view
  75          // with the current theme collection.
  76          this.view = new themes.view.Themes({
  77              collection: this.collection,
  78              parent: this
  79          });
  80  
  81          // Render search form.
  82          this.search();
  83  
  84          this.$el.removeClass( 'search-loading' );
  85  
  86          // Render and append.
  87          this.view.render();
  88          this.$el.empty().append( this.view.el ).addClass( 'rendered' );
  89      },
  90  
  91      // Defines search element container.
  92      searchContainer: $( '.search-form' ),
  93  
  94      // Search input and view
  95      // for current theme collection.
  96      search: function() {
  97          var view,
  98              self = this;
  99  
 100          // Don't render the search if there is only one theme.
 101          if ( themes.data.themes.length === 1 ) {
 102              return;
 103          }
 104  
 105          view = new this.SearchView({
 106              collection: self.collection,
 107              parent: this
 108          });
 109          self.SearchView = view;
 110  
 111          // Render and append after screen title.
 112          view.render();
 113          this.searchContainer
 114              .append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
 115              .append( view.el )
 116              .on( 'submit', function( event ) {
 117                  event.preventDefault();
 118              });
 119      },
 120  
 121      // Checks when the user gets close to the bottom
 122      // of the mage and triggers a theme:scroll event.
 123      scroller: function() {
 124          var self = this,
 125              bottom, threshold;
 126  
 127          bottom = this.window.scrollTop() + self.window.height();
 128          threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
 129          threshold = Math.round( threshold * 0.9 );
 130  
 131          if ( bottom > threshold ) {
 132              this.trigger( 'theme:scroll' );
 133          }
 134      }
 135  });
 136  
 137  // Set up the Collection for our theme data.
 138  // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
 139  themes.Collection = Backbone.Collection.extend({
 140  
 141      model: themes.Model,
 142  
 143      // Search terms.
 144      terms: '',
 145  
 146      // Controls searching on the current theme collection
 147      // and triggers an update event.
 148      doSearch: function( value ) {
 149  
 150          // Don't do anything if we've already done this search.
 151          // Useful because the Search handler fires multiple times per keystroke.
 152          if ( this.terms === value ) {
 153              return;
 154          }
 155  
 156          // Updates terms with the value passed.
 157          this.terms = value;
 158  
 159          // If we have terms, run a search...
 160          if ( this.terms.length > 0 ) {
 161              this.search( this.terms );
 162          }
 163  
 164          // If search is blank, show all themes.
 165          // Useful for resetting the views when you clean the input.
 166          if ( this.terms === '' ) {
 167              this.reset( themes.data.themes );
 168              $( 'body' ).removeClass( 'no-results' );
 169          }
 170  
 171          // Trigger a 'themes:update' event.
 172          this.trigger( 'themes:update' );
 173      },
 174  
 175      /**
 176       * Performs a search within the collection.
 177       *
 178       * @uses RegExp
 179       */
 180      search: function( term ) {
 181          var match, results, haystack, name, description, author;
 182  
 183          // Start with a full collection.
 184          this.reset( themes.data.themes, { silent: true } );
 185  
 186          // Trim the term.
 187          term = term.trim();
 188  
 189          // Escape the term string for RegExp meta characters.
 190          term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
 191  
 192          // Consider spaces as word delimiters and match the whole string
 193          // so matching terms can be combined.
 194          term = term.replace( / /g, ')(?=.*' );
 195          match = new RegExp( '^(?=.*' + term + ').+', 'i' );
 196  
 197          // Find results.
 198          // _.filter() and .test().
 199          results = this.filter( function( data ) {
 200              name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
 201              description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
 202              author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
 203  
 204              haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );
 205  
 206              if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
 207                  data.set( 'displayAuthor', true );
 208              }
 209  
 210              return match.test( haystack );
 211          });
 212  
 213          if ( results.length === 0 ) {
 214              this.trigger( 'query:empty' );
 215          } else {
 216              $( 'body' ).removeClass( 'no-results' );
 217          }
 218  
 219          this.reset( results );
 220      },
 221  
 222      // Paginates the collection with a helper method
 223      // that slices the collection.
 224      paginate: function( instance ) {
 225          var collection = this;
 226          instance = instance || 0;
 227  
 228          // Themes per instance are set at 20.
 229          collection = _( collection.rest( 20 * instance ) );
 230          collection = _( collection.first( 20 ) );
 231  
 232          return collection;
 233      },
 234  
 235      count: false,
 236  
 237      /*
 238       * Handles requests for more themes and caches results.
 239       *
 240       *
 241       * When we are missing a cache object we fire an apiCall()
 242       * which triggers events of `query:success` or `query:fail`.
 243       */
 244      query: function( request ) {
 245          /**
 246           * @static
 247           * @type Array
 248           */
 249          var queries = this.queries,
 250              self = this,
 251              query, isPaginated, count;
 252  
 253          // Store current query request args
 254          // for later use with the event `theme:end`.
 255          this.currentQuery.request = request;
 256  
 257          // Search the query cache for matches.
 258          query = _.find( queries, function( query ) {
 259              return _.isEqual( query.request, request );
 260          });
 261  
 262          // If the request matches the stored currentQuery.request
 263          // it means we have a paginated request.
 264          isPaginated = _.has( request, 'page' );
 265  
 266          // Reset the internal api page counter for non paginated queries.
 267          if ( ! isPaginated ) {
 268              this.currentQuery.page = 1;
 269          }
 270  
 271          // Otherwise, send a new API call and add it to the cache.
 272          if ( ! query && ! isPaginated ) {
 273              query = this.apiCall( request ).done( function( data ) {
 274  
 275                  // Update the collection with the queried data.
 276                  if ( data.themes ) {
 277                      self.reset( data.themes );
 278                      count = data.info.results;
 279                      // Store the results and the query request.
 280                      queries.push( { themes: data.themes, request: request, total: count } );
 281                  }
 282  
 283                  // Trigger a collection refresh event
 284                  // and a `query:success` event with a `count` argument.
 285                  self.trigger( 'themes:update' );
 286                  self.trigger( 'query:success', count );
 287  
 288                  if ( data.themes && data.themes.length === 0 ) {
 289                      self.trigger( 'query:empty' );
 290                  }
 291  
 292              }).fail( function() {
 293                  self.trigger( 'query:fail' );
 294              });
 295          } else {
 296              // If it's a paginated request we need to fetch more themes...
 297              if ( isPaginated ) {
 298                  return this.apiCall( request, isPaginated ).done( function( data ) {
 299                      // Add the new themes to the current collection.
 300                      // @todo Update counter.
 301                      self.add( data.themes );
 302                      self.trigger( 'query:success' );
 303  
 304                      // We are done loading themes for now.
 305                      self.loadingThemes = false;
 306  
 307                  }).fail( function() {
 308                      self.trigger( 'query:fail' );
 309                  });
 310              }
 311  
 312              if ( query.themes.length === 0 ) {
 313                  self.trigger( 'query:empty' );
 314              } else {
 315                  $( 'body' ).removeClass( 'no-results' );
 316              }
 317  
 318              // Only trigger an update event since we already have the themes
 319              // on our cached object.
 320              if ( _.isNumber( query.total ) ) {
 321                  this.count = query.total;
 322              }
 323  
 324              this.reset( query.themes );
 325              if ( ! query.total ) {
 326                  this.count = this.length;
 327              }
 328  
 329              this.trigger( 'themes:update' );
 330              this.trigger( 'query:success', this.count );
 331          }
 332      },
 333  
 334      // Local cache array for API queries.
 335      queries: [],
 336  
 337      // Keep track of current query so we can handle pagination.
 338      currentQuery: {
 339          page: 1,
 340          request: {}
 341      },
 342  
 343      // Send request to api.wordpress.org/themes.
 344      apiCall: function( request, paginated ) {
 345          return wp.ajax.send( 'query-themes', {
 346              data: {
 347                  // Request data.
 348                  request: _.extend({
 349                      per_page: 100
 350                  }, request)
 351              },
 352  
 353              beforeSend: function() {
 354                  if ( ! paginated ) {
 355                      // Spin it.
 356                      $( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
 357                  }
 358              }
 359          });
 360      },
 361  
 362      // Static status controller for when we are loading themes.
 363      loadingThemes: false
 364  });
 365  
 366  // This is the view that controls each theme item
 367  // that will be displayed on the screen.
 368  themes.view.Theme = wp.Backbone.View.extend({
 369  
 370      // Wrap theme data on a div.theme element.
 371      className: 'theme',
 372  
 373      // Reflects which theme view we have.
 374      // 'grid' (default) or 'detail'.
 375      state: 'grid',
 376  
 377      // The HTML template for each element to be rendered.
 378      html: themes.template( 'theme' ),
 379  
 380      events: {
 381          'click': themes.isInstall ? 'preview': 'expand',
 382          'keydown': themes.isInstall ? 'preview': 'expand',
 383          'touchend': themes.isInstall ? 'preview': 'expand',
 384          'keyup': 'addFocus',
 385          'touchmove': 'preventExpand',
 386          'click .theme-install': 'installTheme',
 387          'click .update-message': 'updateTheme'
 388      },
 389  
 390      touchDrag: false,
 391  
 392      initialize: function() {
 393          this.model.on( 'change', this.render, this );
 394      },
 395  
 396      render: function() {
 397          var data = this.model.toJSON();
 398  
 399          // Render themes using the html template.
 400          this.$el.html( this.html( data ) ).attr({
 401              tabindex: 0,
 402              'aria-describedby' : data.id + '-action ' + data.id + '-name',
 403              'data-slug': data.id
 404          });
 405  
 406          // Renders active theme styles.
 407          this.activeTheme();
 408  
 409          if ( this.model.get( 'displayAuthor' ) ) {
 410              this.$el.addClass( 'display-author' );
 411          }
 412      },
 413  
 414      // Adds a class to the currently active theme
 415      // and to the overlay in detailed view mode.
 416      activeTheme: function() {
 417          if ( this.model.get( 'active' ) ) {
 418              this.$el.addClass( 'active' );
 419          }
 420      },
 421  
 422      // Add class of focus to the theme we are focused on.
 423      addFocus: function() {
 424          var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
 425  
 426          $('.theme.focus').removeClass('focus');
 427          $themeToFocus.addClass('focus');
 428      },
 429  
 430      // Single theme overlay screen.
 431      // It's shown when clicking a theme.
 432      expand: function( event ) {
 433          var self = this;
 434  
 435          event = event || window.event;
 436  
 437          // 'Enter' and 'Space' keys expand the details view when a theme is :focused.
 438          if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
 439              return;
 440          }
 441  
 442          // Bail if the user scrolled on a touch device.
 443          if ( this.touchDrag === true ) {
 444              return this.touchDrag = false;
 445          }
 446  
 447          // Prevent the modal from showing when the user clicks
 448          // one of the direct action buttons.
 449          if ( $( event.target ).is( '.theme-actions a' ) ) {
 450              return;
 451          }
 452  
 453          // Prevent the modal from showing when the user clicks one of the direct action buttons.
 454          if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
 455              return;
 456          }
 457  
 458          // Set focused theme to current element.
 459          themes.focusedTheme = this.$el;
 460  
 461          this.trigger( 'theme:expand', self.model.cid );
 462      },
 463  
 464      preventExpand: function() {
 465          this.touchDrag = true;
 466      },
 467  
 468      preview: function( event ) {
 469          var self = this,
 470              current, preview;
 471  
 472          event = event || window.event;
 473  
 474          // Bail if the user scrolled on a touch device.
 475          if ( this.touchDrag === true ) {
 476              return this.touchDrag = false;
 477          }
 478  
 479          // Allow direct link path to installing a theme.
 480          if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
 481              return;
 482          }
 483  
 484          // 'Enter' and 'Space' keys expand the details view when a theme is :focused.
 485          if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
 486              return;
 487          }
 488  
 489          // Pressing Enter while focused on the buttons shouldn't open the preview.
 490          if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
 491              return;
 492          }
 493  
 494          event.preventDefault();
 495  
 496          event = event || window.event;
 497  
 498          // Set focus to current theme.
 499          themes.focusedTheme = this.$el;
 500  
 501          // Construct a new Preview view.
 502          themes.preview = preview = new themes.view.Preview({
 503              model: this.model
 504          });
 505  
 506          // Render the view and append it.
 507          preview.render();
 508          this.setNavButtonsState();
 509  
 510          // Hide previous/next navigation if there is only one theme.
 511          if ( this.model.collection.length === 1 ) {
 512              preview.$el.addClass( 'no-navigation' );
 513          } else {
 514              preview.$el.removeClass( 'no-navigation' );
 515          }
 516  
 517          // Append preview.
 518          $( 'div.wrap' ).append( preview.el );
 519  
 520          // Listen to our preview object
 521          // for `theme:next` and `theme:previous` events.
 522          this.listenTo( preview, 'theme:next', function() {
 523  
 524              // Keep local track of current theme model.
 525              current = self.model;
 526  
 527              // If we have ventured away from current model update the current model position.
 528              if ( ! _.isUndefined( self.current ) ) {
 529                  current = self.current;
 530              }
 531  
 532              // Get next theme model.
 533              self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
 534  
 535              // If we have no more themes, bail.
 536              if ( _.isUndefined( self.current ) ) {
 537                  self.options.parent.parent.trigger( 'theme:end' );
 538                  return self.current = current;
 539              }
 540  
 541              preview.model = self.current;
 542  
 543              // Render and append.
 544              preview.render();
 545              this.setNavButtonsState();
 546              $( '.next-theme' ).focus();
 547          })
 548          .listenTo( preview, 'theme:previous', function() {
 549  
 550              // Keep track of current theme model.
 551              current = self.model;
 552  
 553              // Bail early if we are at the beginning of the collection.
 554              if ( self.model.collection.indexOf( self.current ) === 0 ) {
 555                  return;
 556              }
 557  
 558              // If we have ventured away from current model update the current model position.
 559              if ( ! _.isUndefined( self.current ) ) {
 560                  current = self.current;
 561              }
 562  
 563              // Get previous theme model.
 564              self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
 565  
 566              // If we have no more themes, bail.
 567              if ( _.isUndefined( self.current ) ) {
 568                  return;
 569              }
 570  
 571              preview.model = self.current;
 572  
 573              // Render and append.
 574              preview.render();
 575              this.setNavButtonsState();
 576              $( '.previous-theme' ).focus();
 577          });
 578  
 579          this.listenTo( preview, 'preview:close', function() {
 580              self.current = self.model;
 581          });
 582  
 583      },
 584  
 585      // Handles .disabled classes for previous/next buttons in theme installer preview.
 586      setNavButtonsState: function() {
 587          var $themeInstaller = $( '.theme-install-overlay' ),
 588              current = _.isUndefined( this.current ) ? this.model : this.current,
 589              previousThemeButton = $themeInstaller.find( '.previous-theme' ),
 590              nextThemeButton = $themeInstaller.find( '.next-theme' );
 591  
 592          // Disable previous at the zero position.
 593          if ( 0 === this.model.collection.indexOf( current ) ) {
 594              previousThemeButton
 595                  .addClass( 'disabled' )
 596                  .prop( 'disabled', true );
 597  
 598              nextThemeButton.focus();
 599          }
 600  
 601          // Disable next if the next model is undefined.
 602          if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
 603              nextThemeButton
 604                  .addClass( 'disabled' )
 605                  .prop( 'disabled', true );
 606  
 607              previousThemeButton.focus();
 608          }
 609      },
 610  
 611      installTheme: function( event ) {
 612          var _this = this;
 613  
 614          event.preventDefault();
 615  
 616          wp.updates.maybeRequestFilesystemCredentials( event );
 617  
 618          $( document ).on( 'wp-theme-install-success', function( event, response ) {
 619              if ( _this.model.get( 'id' ) === response.slug ) {
 620                  _this.model.set( { 'installed': true } );
 621              }
 622          } );
 623  
 624          wp.updates.installTheme( {
 625              slug: $( event.target ).data( 'slug' )
 626          } );
 627      },
 628  
 629      updateTheme: function( event ) {
 630          var _this = this;
 631  
 632          if ( ! this.model.get( 'hasPackage' ) ) {
 633              return;
 634          }
 635  
 636          event.preventDefault();
 637  
 638          wp.updates.maybeRequestFilesystemCredentials( event );
 639  
 640          $( document ).on( 'wp-theme-update-success', function( event, response ) {
 641              _this.model.off( 'change', _this.render, _this );
 642              if ( _this.model.get( 'id' ) === response.slug ) {
 643                  _this.model.set( {
 644                      hasUpdate: false,
 645                      version: response.newVersion
 646                  } );
 647              }
 648              _this.model.on( 'change', _this.render, _this );
 649          } );
 650  
 651          wp.updates.updateTheme( {
 652              slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
 653          } );
 654      }
 655  });
 656  
 657  // Theme Details view.
 658  // Sets up a modal overlay with the expanded theme data.
 659  themes.view.Details = wp.Backbone.View.extend({
 660  
 661      // Wrap theme data on a div.theme element.
 662      className: 'theme-overlay',
 663  
 664      events: {
 665          'click': 'collapse',
 666          'click .delete-theme': 'deleteTheme',
 667          'click .left': 'previousTheme',
 668          'click .right': 'nextTheme',
 669          'click #update-theme': 'updateTheme'
 670      },
 671  
 672      // The HTML template for the theme overlay.
 673      html: themes.template( 'theme-single' ),
 674  
 675      render: function() {
 676          var data = this.model.toJSON();
 677          this.$el.html( this.html( data ) );
 678          // Renders active theme styles.
 679          this.activeTheme();
 680          // Set up navigation events.
 681          this.navigation();
 682          // Checks screenshot size.
 683          this.screenshotCheck( this.$el );
 684          // Contain "tabbing" inside the overlay.
 685          this.containFocus( this.$el );
 686      },
 687  
 688      // Adds a class to the currently active theme
 689      // and to the overlay in detailed view mode.
 690      activeTheme: function() {
 691          // Check the model has the active property.
 692          this.$el.toggleClass( 'active', this.model.get( 'active' ) );
 693      },
 694  
 695      // Set initial focus and constrain tabbing within the theme browser modal.
 696      containFocus: function( $el ) {
 697  
 698          // Set initial focus on the primary action control.
 699          _.delay( function() {
 700              $( '.theme-overlay' ).focus();
 701          }, 100 );
 702  
 703          // Constrain tabbing within the modal.
 704          $el.on( 'keydown.wp-themes', function( event ) {
 705              var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(),
 706                  $lastFocusable = $el.find( '.theme-actions a:visible' ).last();
 707  
 708              // Check for the Tab key.
 709              if ( 9 === event.which ) {
 710                  if ( $firstFocusable[0] === event.target && event.shiftKey ) {
 711                      $lastFocusable.focus();
 712                      event.preventDefault();
 713                  } else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
 714                      $firstFocusable.focus();
 715                      event.preventDefault();
 716                  }
 717              }
 718          });
 719      },
 720  
 721      // Single theme overlay screen.
 722      // It's shown when clicking a theme.
 723      collapse: function( event ) {
 724          var self = this,
 725              scroll;
 726  
 727          event = event || window.event;
 728  
 729          // Prevent collapsing detailed view when there is only one theme available.
 730          if ( themes.data.themes.length === 1 ) {
 731              return;
 732          }
 733  
 734          // Detect if the click is inside the overlay and don't close it
 735          // unless the target was the div.back button.
 736          if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
 737  
 738              // Add a temporary closing class while overlay fades out.
 739              $( 'body' ).addClass( 'closing-overlay' );
 740  
 741              // With a quick fade out animation.
 742              this.$el.fadeOut( 130, function() {
 743                  // Clicking outside the modal box closes the overlay.
 744                  $( 'body' ).removeClass( 'closing-overlay' );
 745                  // Handle event cleanup.
 746                  self.closeOverlay();
 747  
 748                  // Get scroll position to avoid jumping to the top.
 749                  scroll = document.body.scrollTop;
 750  
 751                  // Clean the URL structure.
 752                  themes.router.navigate( themes.router.baseUrl( '' ) );
 753  
 754                  // Restore scroll position.
 755                  document.body.scrollTop = scroll;
 756  
 757                  // Return focus to the theme div.
 758                  if ( themes.focusedTheme ) {
 759                      themes.focusedTheme.focus();
 760                  }
 761              });
 762          }
 763      },
 764  
 765      // Handles .disabled classes for next/previous buttons.
 766      navigation: function() {
 767  
 768          // Disable Left/Right when at the start or end of the collection.
 769          if ( this.model.cid === this.model.collection.at(0).cid ) {
 770              this.$el.find( '.left' )
 771                  .addClass( 'disabled' )
 772                  .prop( 'disabled', true );
 773          }
 774          if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
 775              this.$el.find( '.right' )
 776                  .addClass( 'disabled' )
 777                  .prop( 'disabled', true );
 778          }
 779      },
 780  
 781      // Performs the actions to effectively close
 782      // the theme details overlay.
 783      closeOverlay: function() {
 784          $( 'body' ).removeClass( 'modal-open' );
 785          this.remove();
 786          this.unbind();
 787          this.trigger( 'theme:collapse' );
 788      },
 789  
 790      updateTheme: function( event ) {
 791          var _this = this;
 792          event.preventDefault();
 793  
 794          wp.updates.maybeRequestFilesystemCredentials( event );
 795  
 796          $( document ).on( 'wp-theme-update-success', function( event, response ) {
 797              if ( _this.model.get( 'id' ) === response.slug ) {
 798                  _this.model.set( {
 799                      hasUpdate: false,
 800                      version: response.newVersion
 801                  } );
 802              }
 803              _this.render();
 804          } );
 805  
 806          wp.updates.updateTheme( {
 807              slug: $( event.target ).data( 'slug' )
 808          } );
 809      },
 810  
 811      deleteTheme: function( event ) {
 812          var _this = this,
 813              _collection = _this.model.collection,
 814              _themes = themes;
 815          event.preventDefault();
 816  
 817          // Confirmation dialog for deleting a theme.
 818          if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
 819              return;
 820          }
 821  
 822          wp.updates.maybeRequestFilesystemCredentials( event );
 823  
 824          $( document ).one( 'wp-theme-delete-success', function( event, response ) {
 825              _this.$el.find( '.close' ).trigger( 'click' );
 826              $( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
 827                  $( this ).remove();
 828                  _themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
 829  
 830                  $( '.wp-filter-search' ).val( '' );
 831                  _collection.doSearch( '' );
 832                  _collection.remove( _this.model );
 833                  _collection.trigger( 'themes:update' );
 834              } );
 835          } );
 836  
 837          wp.updates.deleteTheme( {
 838              slug: this.model.get( 'id' )
 839          } );
 840      },
 841  
 842      nextTheme: function() {
 843          var self = this;
 844          self.trigger( 'theme:next', self.model.cid );
 845          return false;
 846      },
 847  
 848      previousTheme: function() {
 849          var self = this;
 850          self.trigger( 'theme:previous', self.model.cid );
 851          return false;
 852      },
 853  
 854      // Checks if the theme screenshot is the old 300px width version
 855      // and adds a corresponding class if it's true.
 856      screenshotCheck: function( el ) {
 857          var screenshot, image;
 858  
 859          screenshot = el.find( '.screenshot img' );
 860          image = new Image();
 861          image.src = screenshot.attr( 'src' );
 862  
 863          // Width check.
 864          if ( image.width && image.width <= 300 ) {
 865              el.addClass( 'small-screenshot' );
 866          }
 867      }
 868  });
 869  
 870  // Theme Preview view.
 871  // Sets up a modal overlay with the expanded theme data.
 872  themes.view.Preview = themes.view.Details.extend({
 873  
 874      className: 'wp-full-overlay expanded',
 875      el: '.theme-install-overlay',
 876  
 877      events: {
 878          'click .close-full-overlay': 'close',
 879          'click .collapse-sidebar': 'collapse',
 880          'click .devices button': 'previewDevice',
 881          'click .previous-theme': 'previousTheme',
 882          'click .next-theme': 'nextTheme',
 883          'keyup': 'keyEvent',
 884          'click .theme-install': 'installTheme'
 885      },
 886  
 887      // The HTML template for the theme preview.
 888      html: themes.template( 'theme-preview' ),
 889  
 890      render: function() {
 891          var self = this,
 892              currentPreviewDevice,
 893              data = this.model.toJSON(),
 894              $body = $( document.body );
 895  
 896          $body.attr( 'aria-busy', 'true' );
 897  
 898          this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
 899  
 900          currentPreviewDevice = this.$el.data( 'current-preview-device' );
 901          if ( currentPreviewDevice ) {
 902              self.tooglePreviewDeviceButtons( currentPreviewDevice );
 903          }
 904  
 905          themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } );
 906  
 907          this.$el.fadeIn( 200, function() {
 908              $body.addClass( 'theme-installer-active full-overlay-active' );
 909          });
 910  
 911          this.$el.find( 'iframe' ).one( 'load', function() {
 912              self.iframeLoaded();
 913          });
 914      },
 915  
 916      iframeLoaded: function() {
 917          this.$el.addClass( 'iframe-ready' );
 918          $( document.body ).attr( 'aria-busy', 'false' );
 919      },
 920  
 921      close: function() {
 922          this.$el.fadeOut( 200, function() {
 923              $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
 924  
 925              // Return focus to the theme div.
 926              if ( themes.focusedTheme ) {
 927                  themes.focusedTheme.focus();
 928              }
 929          }).removeClass( 'iframe-ready' );
 930  
 931          // Restore the previous browse tab if available.
 932          if ( themes.router.selectedTab ) {
 933              themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) );
 934              themes.router.selectedTab = false;
 935          } else {
 936              themes.router.navigate( themes.router.baseUrl( '' ) );
 937          }
 938          this.trigger( 'preview:close' );
 939          this.undelegateEvents();
 940          this.unbind();
 941          return false;
 942      },
 943  
 944      collapse: function( event ) {
 945          var $button = $( event.currentTarget );
 946          if ( 'true' === $button.attr( 'aria-expanded' ) ) {
 947              $button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
 948          } else {
 949              $button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
 950          }
 951  
 952          this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
 953          return false;
 954      },
 955  
 956      previewDevice: function( event ) {
 957          var device = $( event.currentTarget ).data( 'device' );
 958  
 959          this.$el
 960              .removeClass( 'preview-desktop preview-tablet preview-mobile' )
 961              .addClass( 'preview-' + device )
 962              .data( 'current-preview-device', device );
 963  
 964          this.tooglePreviewDeviceButtons( device );
 965      },
 966  
 967      tooglePreviewDeviceButtons: function( newDevice ) {
 968          var $devices = $( '.wp-full-overlay-footer .devices' );
 969  
 970          $devices.find( 'button' )
 971              .removeClass( 'active' )
 972              .attr( 'aria-pressed', false );
 973  
 974          $devices.find( 'button.preview-' + newDevice )
 975              .addClass( 'active' )
 976              .attr( 'aria-pressed', true );
 977      },
 978  
 979      keyEvent: function( event ) {
 980          // The escape key closes the preview.
 981          if ( event.keyCode === 27 ) {
 982              this.undelegateEvents();
 983              this.close();
 984          }
 985          // The right arrow key, next theme.
 986          if ( event.keyCode === 39 ) {
 987              _.once( this.nextTheme() );
 988          }
 989  
 990          // The left arrow key, previous theme.
 991          if ( event.keyCode === 37 ) {
 992              this.previousTheme();
 993          }
 994      },
 995  
 996      installTheme: function( event ) {
 997          var _this   = this,
 998              $target = $( event.target );
 999          event.preventDefault();
1000  
1001          if ( $target.hasClass( 'disabled' ) ) {
1002              return;
1003          }
1004  
1005          wp.updates.maybeRequestFilesystemCredentials( event );
1006  
1007          $( document ).on( 'wp-theme-install-success', function() {
1008              _this.model.set( { 'installed': true } );
1009          } );
1010  
1011          wp.updates.installTheme( {
1012              slug: $target.data( 'slug' )
1013          } );
1014      }
1015  });
1016  
1017  // Controls the rendering of div.themes,
1018  // a wrapper that will hold all the theme elements.
1019  themes.view.Themes = wp.Backbone.View.extend({
1020  
1021      className: 'themes wp-clearfix',
1022      $overlay: $( 'div.theme-overlay' ),
1023  
1024      // Number to keep track of scroll position
1025      // while in theme-overlay mode.
1026      index: 0,
1027  
1028      // The theme count element.
1029      count: $( '.wrap .theme-count' ),
1030  
1031      // The live themes count.
1032      liveThemeCount: 0,
1033  
1034      initialize: function( options ) {
1035          var self = this;
1036  
1037          // Set up parent.
1038          this.parent = options.parent;
1039  
1040          // Set current view to [grid].
1041          this.setView( 'grid' );
1042  
1043          // Move the active theme to the beginning of the collection.
1044          self.currentTheme();
1045  
1046          // When the collection is updated by user input...
1047          this.listenTo( self.collection, 'themes:update', function() {
1048              self.parent.page = 0;
1049              self.currentTheme();
1050              self.render( this );
1051          } );
1052  
1053          // Update theme count to full result set when available.
1054          this.listenTo( self.collection, 'query:success', function( count ) {
1055              if ( _.isNumber( count ) ) {
1056                  self.count.text( count );
1057                  self.announceSearchResults( count );
1058              } else {
1059                  self.count.text( self.collection.length );
1060                  self.announceSearchResults( self.collection.length );
1061              }
1062          });
1063  
1064          this.listenTo( self.collection, 'query:empty', function() {
1065              $( 'body' ).addClass( 'no-results' );
1066          });
1067  
1068          this.listenTo( this.parent, 'theme:scroll', function() {
1069              self.renderThemes( self.parent.page );
1070          });
1071  
1072          this.listenTo( this.parent, 'theme:close', function() {
1073              if ( self.overlay ) {
1074                  self.overlay.closeOverlay();
1075              }
1076          } );
1077  
1078          // Bind keyboard events.
1079          $( 'body' ).on( 'keyup', function( event ) {
1080              if ( ! self.overlay ) {
1081                  return;
1082              }
1083  
1084              // Bail if the filesystem credentials dialog is shown.
1085              if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
1086                  return;
1087              }
1088  
1089              // Pressing the right arrow key fires a theme:next event.
1090              if ( event.keyCode === 39 ) {
1091                  self.overlay.nextTheme();
1092              }
1093  
1094              // Pressing the left arrow key fires a theme:previous event.
1095              if ( event.keyCode === 37 ) {
1096                  self.overlay.previousTheme();
1097              }
1098  
1099              // Pressing the escape key fires a theme:collapse event.
1100              if ( event.keyCode === 27 ) {
1101                  self.overlay.collapse( event );
1102              }
1103          });
1104      },
1105  
1106      // Manages rendering of theme pages
1107      // and keeping theme count in sync.
1108      render: function() {
1109          // Clear the DOM, please.
1110          this.$el.empty();
1111  
1112          // If the user doesn't have switch capabilities or there is only one theme
1113          // in the collection, render the detailed view of the active theme.
1114          if ( themes.data.themes.length === 1 ) {
1115  
1116              // Constructs the view.
1117              this.singleTheme = new themes.view.Details({
1118                  model: this.collection.models[0]
1119              });
1120  
1121              // Render and apply a 'single-theme' class to our container.
1122              this.singleTheme.render();
1123              this.$el.addClass( 'single-theme' );
1124              this.$el.append( this.singleTheme.el );
1125          }
1126  
1127          // Generate the themes using page instance
1128          // while checking the collection has items.
1129          if ( this.options.collection.size() > 0 ) {
1130              this.renderThemes( this.parent.page );
1131          }
1132  
1133          // Display a live theme count for the collection.
1134          this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
1135          this.count.text( this.liveThemeCount );
1136  
1137          /*
1138           * In the theme installer the themes count is already announced
1139           * because `announceSearchResults` is called on `query:success`.
1140           */
1141          if ( ! themes.isInstall ) {
1142              this.announceSearchResults( this.liveThemeCount );
1143          }
1144      },
1145  
1146      // Iterates through each instance of the collection
1147      // and renders each theme module.
1148      renderThemes: function( page ) {
1149          var self = this;
1150  
1151          self.instance = self.collection.paginate( page );
1152  
1153          // If we have no more themes, bail.
1154          if ( self.instance.size() === 0 ) {
1155              // Fire a no-more-themes event.
1156              this.parent.trigger( 'theme:end' );
1157              return;
1158          }
1159  
1160          // Make sure the add-new stays at the end.
1161          if ( ! themes.isInstall && page >= 1 ) {
1162              $( '.add-new-theme' ).remove();
1163          }
1164  
1165          // Loop through the themes and setup each theme view.
1166          self.instance.each( function( theme ) {
1167              self.theme = new themes.view.Theme({
1168                  model: theme,
1169                  parent: self
1170              });
1171  
1172              // Render the views...
1173              self.theme.render();
1174              // ...and append them to div.themes.
1175              self.$el.append( self.theme.el );
1176  
1177              // Binds to theme:expand to show the modal box
1178              // with the theme details.
1179              self.listenTo( self.theme, 'theme:expand', self.expand, self );
1180          });
1181  
1182          // 'Add new theme' element shown at the end of the grid.
1183          if ( ! themes.isInstall && themes.data.settings.canInstall ) {
1184              this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' );
1185          }
1186  
1187          this.parent.page++;
1188      },
1189  
1190      // Grabs current theme and puts it at the beginning of the collection.
1191      currentTheme: function() {
1192          var self = this,
1193              current;
1194  
1195          current = self.collection.findWhere({ active: true });
1196  
1197          // Move the active theme to the beginning of the collection.
1198          if ( current ) {
1199              self.collection.remove( current );
1200              self.collection.add( current, { at:0 } );
1201          }
1202      },
1203  
1204      // Sets current view.
1205      setView: function( view ) {
1206          return view;
1207      },
1208  
1209      // Renders the overlay with the ThemeDetails view.
1210      // Uses the current model data.
1211      expand: function( id ) {
1212          var self = this, $card, $modal;
1213  
1214          // Set the current theme model.
1215          this.model = self.collection.get( id );
1216  
1217          // Trigger a route update for the current model.
1218          themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
1219  
1220          // Sets this.view to 'detail'.
1221          this.setView( 'detail' );
1222          $( 'body' ).addClass( 'modal-open' );
1223  
1224          // Set up the theme details view.
1225          this.overlay = new themes.view.Details({
1226              model: self.model
1227          });
1228  
1229          this.overlay.render();
1230  
1231          if ( this.model.get( 'hasUpdate' ) ) {
1232              $card  = $( '[data-slug="' + this.model.id + '"]' );
1233              $modal = $( this.overlay.el );
1234  
1235              if ( $card.find( '.updating-message' ).length ) {
1236                  $modal.find( '.notice-warning h3' ).remove();
1237                  $modal.find( '.notice-warning' )
1238                      .removeClass( 'notice-large' )
1239                      .addClass( 'updating-message' )
1240                      .find( 'p' ).text( wp.updates.l10n.updating );
1241              } else if ( $card.find( '.notice-error' ).length ) {
1242                  $modal.find( '.notice-warning' ).remove();
1243              }
1244          }
1245  
1246          this.$overlay.html( this.overlay.el );
1247  
1248          // Bind to theme:next and theme:previous triggered by the arrow keys.
1249          // Keep track of the current model so we can infer an index position.
1250          this.listenTo( this.overlay, 'theme:next', function() {
1251              // Renders the next theme on the overlay.
1252              self.next( [ self.model.cid ] );
1253  
1254          })
1255          .listenTo( this.overlay, 'theme:previous', function() {
1256              // Renders the previous theme on the overlay.
1257              self.previous( [ self.model.cid ] );
1258          });
1259      },
1260  
1261      /*
1262       * This method renders the next theme on the overlay modal
1263       * based on the current position in the collection.
1264       *
1265       * @params [model cid]
1266       */
1267      next: function( args ) {
1268          var self = this,
1269              model, nextModel;
1270  
1271          // Get the current theme.
1272          model = self.collection.get( args[0] );
1273          // Find the next model within the collection.
1274          nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
1275  
1276          // Sanity check which also serves as a boundary test.
1277          if ( nextModel !== undefined ) {
1278  
1279              // We have a new theme...
1280              // Close the overlay.
1281              this.overlay.closeOverlay();
1282  
1283              // Trigger a route update for the current model.
1284              self.theme.trigger( 'theme:expand', nextModel.cid );
1285  
1286          }
1287      },
1288  
1289      /*
1290       * This method renders the previous theme on the overlay modal
1291       * based on the current position in the collection.
1292       *
1293       * @params [model cid]
1294       */
1295      previous: function( args ) {
1296          var self = this,
1297              model, previousModel;
1298  
1299          // Get the current theme.
1300          model = self.collection.get( args[0] );
1301          // Find the previous model within the collection.
1302          previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
1303  
1304          if ( previousModel !== undefined ) {
1305  
1306              // We have a new theme...
1307              // Close the overlay.
1308              this.overlay.closeOverlay();
1309  
1310              // Trigger a route update for the current model.
1311              self.theme.trigger( 'theme:expand', previousModel.cid );
1312  
1313          }
1314      },
1315  
1316      // Dispatch audible search results feedback message.
1317      announceSearchResults: function( count ) {
1318          if ( 0 === count ) {
1319              wp.a11y.speak( l10n.noThemesFound );
1320          } else {
1321              wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
1322          }
1323      }
1324  });
1325  
1326  // Search input view controller.
1327  themes.view.Search = wp.Backbone.View.extend({
1328  
1329      tagName: 'input',
1330      className: 'wp-filter-search',
1331      id: 'wp-filter-search-input',
1332      searching: false,
1333  
1334      attributes: {
1335          placeholder: l10n.searchPlaceholder,
1336          type: 'search',
1337          'aria-describedby': 'live-search-desc'
1338      },
1339  
1340      events: {
1341          'input': 'search',
1342          'keyup': 'search',
1343          'blur': 'pushState'
1344      },
1345  
1346      initialize: function( options ) {
1347  
1348          this.parent = options.parent;
1349  
1350          this.listenTo( this.parent, 'theme:close', function() {
1351              this.searching = false;
1352          } );
1353  
1354      },
1355  
1356      search: function( event ) {
1357          // Clear on escape.
1358          if ( event.type === 'keyup' && event.which === 27 ) {
1359              event.target.value = '';
1360          }
1361  
1362          // Since doSearch is debounced, it will only run when user input comes to a rest.
1363          this.doSearch( event );
1364      },
1365  
1366      // Runs a search on the theme collection.
1367      doSearch: function( event ) {
1368          var options = {};
1369  
1370          this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
1371  
1372          // if search is initiated and key is not return.
1373          if ( this.searching && event.which !== 13 ) {
1374              options.replace = true;
1375          } else {
1376              this.searching = true;
1377          }
1378  
1379          // Update the URL hash.
1380          if ( event.target.value ) {
1381              themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
1382          } else {
1383              themes.router.navigate( themes.router.baseUrl( '' ) );
1384          }
1385      },
1386  
1387      pushState: function( event ) {
1388          var url = themes.router.baseUrl( '' );
1389  
1390          if ( event.target.value ) {
1391              url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) );
1392          }
1393  
1394          this.searching = false;
1395          themes.router.navigate( url );
1396  
1397      }
1398  });
1399  
1400  /**
1401   * Navigate router.
1402   *
1403   * @since 4.9.0
1404   *
1405   * @param {string} url - URL to navigate to.
1406   * @param {object} state - State.
1407   * @return {void}
1408   */
1409  function navigateRouter( url, state ) {
1410      var router = this;
1411      if ( Backbone.history._hasPushState ) {
1412          Backbone.Router.prototype.navigate.call( router, url, state );
1413      }
1414  }
1415  
1416  // Sets up the routes events for relevant url queries.
1417  // Listens to [theme] and [search] params.
1418  themes.Router = Backbone.Router.extend({
1419  
1420      routes: {
1421          'themes.php?theme=:slug': 'theme',
1422          'themes.php?search=:query': 'search',
1423          'themes.php?s=:query': 'search',
1424          'themes.php': 'themes',
1425          '': 'themes'
1426      },
1427  
1428      baseUrl: function( url ) {
1429          return 'themes.php' + url;
1430      },
1431  
1432      themePath: '?theme=',
1433      searchPath: '?search=',
1434  
1435      search: function( query ) {
1436          $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
1437      },
1438  
1439      themes: function() {
1440          $( '.wp-filter-search' ).val( '' );
1441      },
1442  
1443      navigate: navigateRouter
1444  
1445  });
1446  
1447  // Execute and setup the application.
1448  themes.Run = {
1449      init: function() {
1450          // Initializes the blog's theme library view.
1451          // Create a new collection with data.
1452          this.themes = new themes.Collection( themes.data.themes );
1453  
1454          // Set up the view.
1455          this.view = new themes.view.Appearance({
1456              collection: this.themes
1457          });
1458  
1459          this.render();
1460  
1461          // Start debouncing user searches after Backbone.history.start().
1462          this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
1463      },
1464  
1465      render: function() {
1466  
1467          // Render results.
1468          this.view.render();
1469          this.routes();
1470  
1471          if ( Backbone.History.started ) {
1472              Backbone.history.stop();
1473          }
1474          Backbone.history.start({
1475              root: themes.data.settings.adminUrl,
1476              pushState: true,
1477              hashChange: false
1478          });
1479      },
1480  
1481      routes: function() {
1482          var self = this;
1483          // Bind to our global thx object
1484          // so that the object is available to sub-views.
1485          themes.router = new themes.Router();
1486  
1487          // Handles theme details route event.
1488          themes.router.on( 'route:theme', function( slug ) {
1489              self.view.view.expand( slug );
1490          });
1491  
1492          themes.router.on( 'route:themes', function() {
1493              self.themes.doSearch( '' );
1494              self.view.trigger( 'theme:close' );
1495          });
1496  
1497          // Handles search route event.
1498          themes.router.on( 'route:search', function() {
1499              $( '.wp-filter-search' ).trigger( 'keyup' );
1500          });
1501  
1502          this.extraRoutes();
1503      },
1504  
1505      extraRoutes: function() {
1506          return false;
1507      }
1508  };
1509  
1510  // Extend the main Search view.
1511  themes.view.InstallerSearch =  themes.view.Search.extend({
1512  
1513      events: {
1514          'input': 'search',
1515          'keyup': 'search'
1516      },
1517  
1518      terms: '',
1519  
1520      // Handles Ajax request for searching through themes in public repo.
1521      search: function( event ) {
1522  
1523          // Tabbing or reverse tabbing into the search input shouldn't trigger a search.
1524          if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
1525              return;
1526          }
1527  
1528          this.collection = this.options.parent.view.collection;
1529  
1530          // Clear on escape.
1531          if ( event.type === 'keyup' && event.which === 27 ) {
1532              event.target.value = '';
1533          }
1534  
1535          this.doSearch( event.target.value );
1536      },
1537  
1538      doSearch: function( value ) {
1539          var request = {};
1540  
1541          // Don't do anything if the search terms haven't changed.
1542          if ( this.terms === value ) {
1543              return;
1544          }
1545  
1546          // Updates terms with the value passed.
1547          this.terms = value;
1548  
1549          request.search = value;
1550  
1551          /*
1552           * Intercept an [author] search.
1553           *
1554           * If input value starts with `author:` send a request
1555           * for `author` instead of a regular `search`.
1556           */
1557          if ( value.substring( 0, 7 ) === 'author:' ) {
1558              request.search = '';
1559              request.author = value.slice( 7 );
1560          }
1561  
1562          /*
1563           * Intercept a [tag] search.
1564           *
1565           * If input value starts with `tag:` send a request
1566           * for `tag` instead of a regular `search`.
1567           */
1568          if ( value.substring( 0, 4 ) === 'tag:' ) {
1569              request.search = '';
1570              request.tag = [ value.slice( 4 ) ];
1571          }
1572  
1573          $( '.filter-links li > a.current' )
1574              .removeClass( 'current' )
1575              .removeAttr( 'aria-current' );
1576  
1577          $( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
1578          $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
1579  
1580          // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1581          // or searching the local cache.
1582          this.collection.query( request );
1583  
1584          // Set route.
1585          themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
1586      }
1587  });
1588  
1589  themes.view.Installer = themes.view.Appearance.extend({
1590  
1591      el: '#wpbody-content .wrap',
1592  
1593      // Register events for sorting and filters in theme-navigation.
1594      events: {
1595          'click .filter-links li > a': 'onSort',
1596          'click .theme-filter': 'onFilter',
1597          'click .drawer-toggle': 'moreFilters',
1598          'click .filter-drawer .apply-filters': 'applyFilters',
1599          'click .filter-group [type="checkbox"]': 'addFilter',
1600          'click .filter-drawer .clear-filters': 'clearFilters',
1601          'click .edit-filters': 'backToFilters',
1602          'click .favorites-form-submit' : 'saveUsername',
1603          'keyup #wporg-username-input': 'saveUsername'
1604      },
1605  
1606      // Initial render method.
1607      render: function() {
1608          var self = this;
1609  
1610          this.search();
1611          this.uploader();
1612  
1613          this.collection = new themes.Collection();
1614  
1615          // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
1616          this.listenTo( this, 'theme:end', function() {
1617  
1618              // Make sure we are not already loading.
1619              if ( self.collection.loadingThemes ) {
1620                  return;
1621              }
1622  
1623              // Set loadingThemes to true and bump page instance of currentQuery.
1624              self.collection.loadingThemes = true;
1625              self.collection.currentQuery.page++;
1626  
1627              // Use currentQuery.page to build the themes request.
1628              _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
1629              self.collection.query( self.collection.currentQuery.request );
1630          });
1631  
1632          this.listenTo( this.collection, 'query:success', function() {
1633              $( 'body' ).removeClass( 'loading-content' );
1634              $( '.theme-browser' ).find( 'div.error' ).remove();
1635          });
1636  
1637          this.listenTo( this.collection, 'query:fail', function() {
1638              $( 'body' ).removeClass( 'loading-content' );
1639              $( '.theme-browser' ).find( 'div.error' ).remove();
1640              $( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
1641              $( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
1642                  e.preventDefault();
1643                  $( 'input.wp-filter-search' ).trigger( 'input' );
1644              } );
1645          });
1646  
1647          if ( this.view ) {
1648              this.view.remove();
1649          }
1650  
1651          // Sets up the view and passes the section argument.
1652          this.view = new themes.view.Themes({
1653              collection: this.collection,
1654              parent: this
1655          });
1656  
1657          // Reset pagination every time the install view handler is run.
1658          this.page = 0;
1659  
1660          // Render and append.
1661          this.$el.find( '.themes' ).remove();
1662          this.view.render();
1663          this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
1664      },
1665  
1666      // Handles all the rendering of the public theme directory.
1667      browse: function( section ) {
1668          // Create a new collection with the proper theme data
1669          // for each section.
1670          this.collection.query( { browse: section } );
1671      },
1672  
1673      // Sorting navigation.
1674      onSort: function( event ) {
1675          var $el = $( event.target ),
1676              sort = $el.data( 'sort' );
1677  
1678          event.preventDefault();
1679  
1680          $( 'body' ).removeClass( 'filters-applied show-filters' );
1681          $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
1682  
1683          // Bail if this is already active.
1684          if ( $el.hasClass( this.activeClass ) ) {
1685              return;
1686          }
1687  
1688          this.sort( sort );
1689  
1690          // Trigger a router.navigate update.
1691          themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
1692      },
1693  
1694      sort: function( sort ) {
1695          this.clearSearch();
1696  
1697          // Track sorting so we can restore the correct tab when closing preview.
1698          themes.router.selectedTab = sort;
1699  
1700          $( '.filter-links li > a, .theme-filter' )
1701              .removeClass( this.activeClass )
1702              .removeAttr( 'aria-current' );
1703  
1704          $( '[data-sort="' + sort + '"]' )
1705              .addClass( this.activeClass )
1706              .attr( 'aria-current', 'page' );
1707  
1708          if ( 'favorites' === sort ) {
1709              $( 'body' ).addClass( 'show-favorites-form' );
1710          } else {
1711              $( 'body' ).removeClass( 'show-favorites-form' );
1712          }
1713  
1714          this.browse( sort );
1715      },
1716  
1717      // Filters and Tags.
1718      onFilter: function( event ) {
1719          var request,
1720              $el = $( event.target ),
1721              filter = $el.data( 'filter' );
1722  
1723          // Bail if this is already active.
1724          if ( $el.hasClass( this.activeClass ) ) {
1725              return;
1726          }
1727  
1728          $( '.filter-links li > a, .theme-section' )
1729              .removeClass( this.activeClass )
1730              .removeAttr( 'aria-current' );
1731          $el
1732              .addClass( this.activeClass )
1733              .attr( 'aria-current', 'page' );
1734  
1735          if ( ! filter ) {
1736              return;
1737          }
1738  
1739          // Construct the filter request
1740          // using the default values.
1741          filter = _.union( [ filter, this.filtersChecked() ] );
1742          request = { tag: [ filter ] };
1743  
1744          // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1745          // or searching the local cache.
1746          this.collection.query( request );
1747      },
1748  
1749      // Clicking on a checkbox to add another filter to the request.
1750      addFilter: function() {
1751          this.filtersChecked();
1752      },
1753  
1754      // Applying filters triggers a tag request.
1755      applyFilters: function( event ) {
1756          var name,
1757              tags = this.filtersChecked(),
1758              request = { tag: tags },
1759              filteringBy = $( '.filtered-by .tags' );
1760  
1761          if ( event ) {
1762              event.preventDefault();
1763          }
1764  
1765          if ( ! tags ) {
1766              wp.a11y.speak( l10n.selectFeatureFilter );
1767              return;
1768          }
1769  
1770          $( 'body' ).addClass( 'filters-applied' );
1771          $( '.filter-links li > a.current' )
1772              .removeClass( 'current' )
1773              .removeAttr( 'aria-current' );
1774  
1775          filteringBy.empty();
1776  
1777          _.each( tags, function( tag ) {
1778              name = $( 'label[for="filter-id-' + tag + '"]' ).text();
1779              filteringBy.append( '<span class="tag">' + name + '</span>' );
1780          });
1781  
1782          // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1783          // or searching the local cache.
1784          this.collection.query( request );
1785      },
1786  
1787      // Save the user's WordPress.org username and get his favorite themes.
1788      saveUsername: function ( event ) {
1789          var username = $( '#wporg-username-input' ).val(),
1790              nonce = $( '#wporg-username-nonce' ).val(),
1791              request = { browse: 'favorites', user: username },
1792              that = this;
1793  
1794          if ( event ) {
1795              event.preventDefault();
1796          }
1797  
1798          // Save username on enter.
1799          if ( event.type === 'keyup' && event.which !== 13 ) {
1800              return;
1801          }
1802  
1803          return wp.ajax.send( 'save-wporg-username', {
1804              data: {
1805                  _wpnonce: nonce,
1806                  username: username
1807              },
1808              success: function () {
1809                  // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1810                  // or searching the local cache.
1811                  that.collection.query( request );
1812              }
1813          } );
1814      },
1815  
1816      /**
1817       * Get the checked filters.
1818       *
1819       * @return {array} of tags or false
1820       */
1821      filtersChecked: function() {
1822          var items = $( '.filter-group' ).find( ':checkbox' ),
1823              tags = [];
1824  
1825          _.each( items.filter( ':checked' ), function( item ) {
1826              tags.push( $( item ).prop( 'value' ) );
1827          });
1828  
1829          // When no filters are checked, restore initial state and return.
1830          if ( tags.length === 0 ) {
1831              $( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
1832              $( '.filter-drawer .clear-filters' ).hide();
1833              $( 'body' ).removeClass( 'filters-applied' );
1834              return false;
1835          }
1836  
1837          $( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
1838          $( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
1839  
1840          return tags;
1841      },
1842  
1843      activeClass: 'current',
1844  
1845      /**
1846       * When users press the "Upload Theme" button, show the upload form in place.
1847       */
1848      uploader: function() {
1849          var uploadViewToggle = $( '.upload-view-toggle' ),
1850              $body = $( document.body );
1851  
1852          uploadViewToggle.on( 'click', function() {
1853              // Toggle the upload view.
1854              $body.toggleClass( 'show-upload-view' );
1855              // Toggle the `aria-expanded` button attribute.
1856              uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
1857          });
1858      },
1859  
1860      // Toggle the full filters navigation.
1861      moreFilters: function( event ) {
1862          var $body = $( 'body' ),
1863              $toggleButton = $( '.drawer-toggle' );
1864  
1865          event.preventDefault();
1866  
1867          if ( $body.hasClass( 'filters-applied' ) ) {
1868              return this.backToFilters();
1869          }
1870  
1871          this.clearSearch();
1872  
1873          themes.router.navigate( themes.router.baseUrl( '' ) );
1874          // Toggle the feature filters view.
1875          $body.toggleClass( 'show-filters' );
1876          // Toggle the `aria-expanded` button attribute.
1877          $toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
1878      },
1879  
1880      /**
1881       * Clears all the checked filters.
1882       *
1883       * @uses filtersChecked()
1884       */
1885      clearFilters: function( event ) {
1886          var items = $( '.filter-group' ).find( ':checkbox' ),
1887              self = this;
1888  
1889          event.preventDefault();
1890  
1891          _.each( items.filter( ':checked' ), function( item ) {
1892              $( item ).prop( 'checked', false );
1893              return self.filtersChecked();
1894          });
1895      },
1896  
1897      backToFilters: function( event ) {
1898          if ( event ) {
1899              event.preventDefault();
1900          }
1901  
1902          $( 'body' ).removeClass( 'filters-applied' );
1903      },
1904  
1905      clearSearch: function() {
1906          $( '#wp-filter-search-input').val( '' );
1907      }
1908  });
1909  
1910  themes.InstallerRouter = Backbone.Router.extend({
1911      routes: {
1912          'theme-install.php?theme=:slug': 'preview',
1913          'theme-install.php?browse=:sort': 'sort',
1914          'theme-install.php?search=:query': 'search',
1915          'theme-install.php': 'sort'
1916      },
1917  
1918      baseUrl: function( url ) {
1919          return 'theme-install.php' + url;
1920      },
1921  
1922      themePath: '?theme=',
1923      browsePath: '?browse=',
1924      searchPath: '?search=',
1925  
1926      search: function( query ) {
1927          $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
1928      },
1929  
1930      navigate: navigateRouter
1931  });
1932  
1933  
1934  themes.RunInstaller = {
1935  
1936      init: function() {
1937          // Set up the view.
1938          // Passes the default 'section' as an option.
1939          this.view = new themes.view.Installer({
1940              section: 'featured',
1941              SearchView: themes.view.InstallerSearch
1942          });
1943  
1944          // Render results.
1945          this.render();
1946  
1947          // Start debouncing user searches after Backbone.history.start().
1948          this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
1949      },
1950  
1951      render: function() {
1952  
1953          // Render results.
1954          this.view.render();
1955          this.routes();
1956  
1957          if ( Backbone.History.started ) {
1958              Backbone.history.stop();
1959          }
1960          Backbone.history.start({
1961              root: themes.data.settings.adminUrl,
1962              pushState: true,
1963              hashChange: false
1964          });
1965      },
1966  
1967      routes: function() {
1968          var self = this,
1969              request = {};
1970  
1971          // Bind to our global `wp.themes` object
1972          // so that the router is available to sub-views.
1973          themes.router = new themes.InstallerRouter();
1974  
1975          // Handles `theme` route event.
1976          // Queries the API for the passed theme slug.
1977          themes.router.on( 'route:preview', function( slug ) {
1978  
1979              // Remove existing handlers.
1980              if ( themes.preview ) {
1981                  themes.preview.undelegateEvents();
1982                  themes.preview.unbind();
1983              }
1984  
1985              // If the theme preview is active, set the current theme.
1986              if ( self.view.view.theme && self.view.view.theme.preview ) {
1987                  self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } );
1988                  self.view.view.theme.preview();
1989              } else {
1990  
1991                  // Select the theme by slug.
1992                  request.theme = slug;
1993                  self.view.collection.query( request );
1994                  self.view.collection.trigger( 'update' );
1995  
1996                  // Open the theme preview.
1997                  self.view.collection.once( 'query:success', function() {
1998                      $( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
1999                  });
2000  
2001              }
2002          });
2003  
2004          /*
2005           * Handles sorting / browsing routes.
2006           * Also handles the root URL triggering a sort request
2007           * for `featured`, the default view.
2008           */
2009          themes.router.on( 'route:sort', function( sort ) {
2010              if ( ! sort ) {
2011                  sort = 'featured';
2012                  themes.router.navigate( themes.router.baseUrl( '?browse=featured' ), { replace: true } );
2013              }
2014              self.view.sort( sort );
2015  
2016              // Close the preview if open.
2017              if ( themes.preview ) {
2018                  themes.preview.close();
2019              }
2020          });
2021  
2022          // The `search` route event. The router populates the input field.
2023          themes.router.on( 'route:search', function() {
2024              $( '.wp-filter-search' ).focus().trigger( 'keyup' );
2025          });
2026  
2027          this.extraRoutes();
2028      },
2029  
2030      extraRoutes: function() {
2031          return false;
2032      }
2033  };
2034  
2035  // Ready...
2036  $( document ).ready(function() {
2037      if ( themes.isInstall ) {
2038          themes.RunInstaller.init();
2039      } else {
2040          themes.Run.init();
2041      }
2042  
2043      // Update the return param just in time.
2044      $( document.body ).on( 'click', '.load-customize', function() {
2045          var link = $( this ), urlParser = document.createElement( 'a' );
2046          urlParser.href = link.prop( 'href' );
2047          urlParser.search = $.param( _.extend(
2048              wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ),
2049              {
2050                  'return': window.location.href
2051              }
2052          ) );
2053          link.prop( 'href', urlParser.href );
2054      });
2055  
2056      $( '.broken-themes .delete-theme' ).on( 'click', function() {
2057          return confirm( _wpThemeSettings.settings.confirmDelete );
2058      });
2059  });
2060  
2061  })( jQuery );
2062  
2063  // Align theme browser thickbox.
2064  jQuery(document).ready( function($) {
2065      window.tb_position = function() {
2066          var tbWindow = $('#TB_window'),
2067              width = $(window).width(),
2068              H = $(window).height(),
2069              W = ( 1040 < width ) ? 1040 : width,
2070              adminbar_height = 0;
2071  
2072          if ( $('#wpadminbar').length ) {
2073              adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
2074          }
2075  
2076          if ( tbWindow.size() ) {
2077              tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
2078              $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
2079              tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
2080              if ( typeof document.body.style.maxWidth !== 'undefined' ) {
2081                  tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
2082              }
2083          }
2084      };
2085  
2086      $(window).resize(function(){ tb_position(); });
2087  });


Generated: Thu Feb 27 01:00:03 2020 Cross-referenced by PHPXref 0.7.1