[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

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


Generated : Wed Jul 30 08:20:01 2025 Cross-referenced by PHPXref