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


Generated: Fri Aug 7 01:00:03 2020 Cross-referenced by PHPXref 0.7.1