[ 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          // The right arrow key, next theme.
1015          if ( event.keyCode === 39 ) {
1016              _.once( this.nextTheme() );
1017          }
1018  
1019          // The left arrow key, previous theme.
1020          if ( event.keyCode === 37 ) {
1021              this.previousTheme();
1022          }
1023      },
1024  
1025      installTheme: function( event ) {
1026          var _this   = this,
1027              $target = $( event.target );
1028          event.preventDefault();
1029  
1030          if ( $target.hasClass( 'disabled' ) ) {
1031              return;
1032          }
1033  
1034          wp.updates.maybeRequestFilesystemCredentials( event );
1035  
1036          $( document ).on( 'wp-theme-install-success', function() {
1037              _this.model.set( { 'installed': true } );
1038          } );
1039  
1040          wp.updates.installTheme( {
1041              slug: $target.data( 'slug' )
1042          } );
1043      }
1044  });
1045  
1046  // Controls the rendering of div.themes,
1047  // a wrapper that will hold all the theme elements.
1048  themes.view.Themes = wp.Backbone.View.extend({
1049  
1050      className: 'themes wp-clearfix',
1051      $overlay: $( 'div.theme-overlay' ),
1052  
1053      // Number to keep track of scroll position
1054      // while in theme-overlay mode.
1055      index: 0,
1056  
1057      // The theme count element.
1058      count: $( '.wrap .theme-count' ),
1059  
1060      // The live themes count.
1061      liveThemeCount: 0,
1062  
1063      initialize: function( options ) {
1064          var self = this;
1065  
1066          // Set up parent.
1067          this.parent = options.parent;
1068  
1069          // Set current view to [grid].
1070          this.setView( 'grid' );
1071  
1072          // Move the active theme to the beginning of the collection.
1073          self.currentTheme();
1074  
1075          // When the collection is updated by user input...
1076          this.listenTo( self.collection, 'themes:update', function() {
1077              self.parent.page = 0;
1078              self.currentTheme();
1079              self.render( this );
1080          } );
1081  
1082          // Update theme count to full result set when available.
1083          this.listenTo( self.collection, 'query:success', function( count ) {
1084              if ( _.isNumber( count ) ) {
1085                  self.count.text( count );
1086                  self.announceSearchResults( count );
1087              } else {
1088                  self.count.text( self.collection.length );
1089                  self.announceSearchResults( self.collection.length );
1090              }
1091          });
1092  
1093          this.listenTo( self.collection, 'query:empty', function() {
1094              $( 'body' ).addClass( 'no-results' );
1095          });
1096  
1097          this.listenTo( this.parent, 'theme:scroll', function() {
1098              self.renderThemes( self.parent.page );
1099          });
1100  
1101          this.listenTo( this.parent, 'theme:close', function() {
1102              if ( self.overlay ) {
1103                  self.overlay.closeOverlay();
1104              }
1105          } );
1106  
1107          // Bind keyboard events.
1108          $( 'body' ).on( 'keyup', function( event ) {
1109              if ( ! self.overlay ) {
1110                  return;
1111              }
1112  
1113              // Bail if the filesystem credentials dialog is shown.
1114              if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
1115                  return;
1116              }
1117  
1118              // Pressing the right arrow key fires a theme:next event.
1119              if ( event.keyCode === 39 ) {
1120                  self.overlay.nextTheme();
1121              }
1122  
1123              // Pressing the left arrow key fires a theme:previous event.
1124              if ( event.keyCode === 37 ) {
1125                  self.overlay.previousTheme();
1126              }
1127  
1128              // Pressing the escape key fires a theme:collapse event.
1129              if ( event.keyCode === 27 ) {
1130                  self.overlay.collapse( event );
1131              }
1132          });
1133      },
1134  
1135      // Manages rendering of theme pages
1136      // and keeping theme count in sync.
1137      render: function() {
1138          // Clear the DOM, please.
1139          this.$el.empty();
1140  
1141          // If the user doesn't have switch capabilities or there is only one theme
1142          // in the collection, render the detailed view of the active theme.
1143          if ( themes.data.themes.length === 1 ) {
1144  
1145              // Constructs the view.
1146              this.singleTheme = new themes.view.Details({
1147                  model: this.collection.models[0]
1148              });
1149  
1150              // Render and apply a 'single-theme' class to our container.
1151              this.singleTheme.render();
1152              this.$el.addClass( 'single-theme' );
1153              this.$el.append( this.singleTheme.el );
1154          }
1155  
1156          // Generate the themes using page instance
1157          // while checking the collection has items.
1158          if ( this.options.collection.size() > 0 ) {
1159              this.renderThemes( this.parent.page );
1160          }
1161  
1162          // Display a live theme count for the collection.
1163          this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
1164          this.count.text( this.liveThemeCount );
1165  
1166          /*
1167           * In the theme installer the themes count is already announced
1168           * because `announceSearchResults` is called on `query:success`.
1169           */
1170          if ( ! themes.isInstall ) {
1171              this.announceSearchResults( this.liveThemeCount );
1172          }
1173      },
1174  
1175      // Iterates through each instance of the collection
1176      // and renders each theme module.
1177      renderThemes: function( page ) {
1178          var self = this;
1179  
1180          self.instance = self.collection.paginate( page );
1181  
1182          // If we have no more themes, bail.
1183          if ( self.instance.size() === 0 ) {
1184              // Fire a no-more-themes event.
1185              this.parent.trigger( 'theme:end' );
1186              return;
1187          }
1188  
1189          // Make sure the add-new stays at the end.
1190          if ( ! themes.isInstall && page >= 1 ) {
1191              $( '.add-new-theme' ).remove();
1192          }
1193  
1194          // Loop through the themes and setup each theme view.
1195          self.instance.each( function( theme ) {
1196              self.theme = new themes.view.Theme({
1197                  model: theme,
1198                  parent: self
1199              });
1200  
1201              // Render the views...
1202              self.theme.render();
1203              // ...and append them to div.themes.
1204              self.$el.append( self.theme.el );
1205  
1206              // Binds to theme:expand to show the modal box
1207              // with the theme details.
1208              self.listenTo( self.theme, 'theme:expand', self.expand, self );
1209          });
1210  
1211          // 'Add new theme' element shown at the end of the grid.
1212          if ( ! themes.isInstall && themes.data.settings.canInstall ) {
1213              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>' );
1214          }
1215  
1216          this.parent.page++;
1217      },
1218  
1219      // Grabs current theme and puts it at the beginning of the collection.
1220      currentTheme: function() {
1221          var self = this,
1222              current;
1223  
1224          current = self.collection.findWhere({ active: true });
1225  
1226          // Move the active theme to the beginning of the collection.
1227          if ( current ) {
1228              self.collection.remove( current );
1229              self.collection.add( current, { at:0 } );
1230          }
1231      },
1232  
1233      // Sets current view.
1234      setView: function( view ) {
1235          return view;
1236      },
1237  
1238      // Renders the overlay with the ThemeDetails view.
1239      // Uses the current model data.
1240      expand: function( id ) {
1241          var self = this, $card, $modal;
1242  
1243          // Set the current theme model.
1244          this.model = self.collection.get( id );
1245  
1246          // Trigger a route update for the current model.
1247          themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
1248  
1249          // Sets this.view to 'detail'.
1250          this.setView( 'detail' );
1251          $( 'body' ).addClass( 'modal-open' );
1252  
1253          // Set up the theme details view.
1254          this.overlay = new themes.view.Details({
1255              model: self.model
1256          });
1257  
1258          this.overlay.render();
1259  
1260          if ( this.model.get( 'hasUpdate' ) ) {
1261              $card  = $( '[data-slug="' + this.model.id + '"]' );
1262              $modal = $( this.overlay.el );
1263  
1264              if ( $card.find( '.updating-message' ).length ) {
1265                  $modal.find( '.notice-warning h3' ).remove();
1266                  $modal.find( '.notice-warning' )
1267                      .removeClass( 'notice-large' )
1268                      .addClass( 'updating-message' )
1269                      .find( 'p' ).text( wp.updates.l10n.updating );
1270              } else if ( $card.find( '.notice-error' ).length ) {
1271                  $modal.find( '.notice-warning' ).remove();
1272              }
1273          }
1274  
1275          this.$overlay.html( this.overlay.el );
1276  
1277          // Bind to theme:next and theme:previous triggered by the arrow keys.
1278          // Keep track of the current model so we can infer an index position.
1279          this.listenTo( this.overlay, 'theme:next', function() {
1280              // Renders the next theme on the overlay.
1281              self.next( [ self.model.cid ] );
1282  
1283          })
1284          .listenTo( this.overlay, 'theme:previous', function() {
1285              // Renders the previous theme on the overlay.
1286              self.previous( [ self.model.cid ] );
1287          });
1288      },
1289  
1290      /*
1291       * This method renders the next theme on the overlay modal
1292       * based on the current position in the collection.
1293       *
1294       * @params [model cid]
1295       */
1296      next: function( args ) {
1297          var self = this,
1298              model, nextModel;
1299  
1300          // Get the current theme.
1301          model = self.collection.get( args[0] );
1302          // Find the next model within the collection.
1303          nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
1304  
1305          // Confidence check which also serves as a boundary test.
1306          if ( nextModel !== undefined ) {
1307  
1308              // We have a new theme...
1309              // Close the overlay.
1310              this.overlay.closeOverlay();
1311  
1312              // Trigger a route update for the current model.
1313              self.theme.trigger( 'theme:expand', nextModel.cid );
1314  
1315          }
1316      },
1317  
1318      /*
1319       * This method renders the previous theme on the overlay modal
1320       * based on the current position in the collection.
1321       *
1322       * @params [model cid]
1323       */
1324      previous: function( args ) {
1325          var self = this,
1326              model, previousModel;
1327  
1328          // Get the current theme.
1329          model = self.collection.get( args[0] );
1330          // Find the previous model within the collection.
1331          previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
1332  
1333          if ( previousModel !== undefined ) {
1334  
1335              // We have a new theme...
1336              // Close the overlay.
1337              this.overlay.closeOverlay();
1338  
1339              // Trigger a route update for the current model.
1340              self.theme.trigger( 'theme:expand', previousModel.cid );
1341  
1342          }
1343      },
1344  
1345      // Dispatch audible search results feedback message.
1346      announceSearchResults: function( count ) {
1347          if ( 0 === count ) {
1348              wp.a11y.speak( l10n.noThemesFound );
1349          } else {
1350              wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
1351          }
1352      }
1353  });
1354  
1355  // Search input view controller.
1356  themes.view.Search = wp.Backbone.View.extend({
1357  
1358      tagName: 'input',
1359      className: 'wp-filter-search',
1360      id: 'wp-filter-search-input',
1361      searching: false,
1362  
1363      attributes: {
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          if ( 'block-themes' === section ) {
1699              // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1700              // or searching the local cache.
1701              this.collection.query( { tag: 'full-site-editing' } );
1702          } else {
1703              this.collection.query( { browse: section } );
1704          }
1705      },
1706  
1707      // Sorting navigation.
1708      onSort: function( event ) {
1709          var $el = $( event.target ),
1710              sort = $el.data( 'sort' );
1711  
1712          event.preventDefault();
1713  
1714          $( 'body' ).removeClass( 'filters-applied show-filters' );
1715          $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
1716  
1717          // Bail if this is already active.
1718          if ( $el.hasClass( this.activeClass ) ) {
1719              return;
1720          }
1721  
1722          this.sort( sort );
1723  
1724          // Trigger a router.navigate update.
1725          themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
1726      },
1727  
1728      sort: function( sort ) {
1729          this.clearSearch();
1730  
1731          // Track sorting so we can restore the correct tab when closing preview.
1732          themes.router.selectedTab = sort;
1733  
1734          $( '.filter-links li > a, .theme-filter' )
1735              .removeClass( this.activeClass )
1736              .removeAttr( 'aria-current' );
1737  
1738          $( '[data-sort="' + sort + '"]' )
1739              .addClass( this.activeClass )
1740              .attr( 'aria-current', 'page' );
1741  
1742          if ( 'favorites' === sort ) {
1743              $( 'body' ).addClass( 'show-favorites-form' );
1744          } else {
1745              $( 'body' ).removeClass( 'show-favorites-form' );
1746          }
1747  
1748          this.browse( sort );
1749      },
1750  
1751      // Filters and Tags.
1752      onFilter: function( event ) {
1753          var request,
1754              $el = $( event.target ),
1755              filter = $el.data( 'filter' );
1756  
1757          // Bail if this is already active.
1758          if ( $el.hasClass( this.activeClass ) ) {
1759              return;
1760          }
1761  
1762          $( '.filter-links li > a, .theme-section' )
1763              .removeClass( this.activeClass )
1764              .removeAttr( 'aria-current' );
1765          $el
1766              .addClass( this.activeClass )
1767              .attr( 'aria-current', 'page' );
1768  
1769          if ( ! filter ) {
1770              return;
1771          }
1772  
1773          // Construct the filter request
1774          // using the default values.
1775          filter = _.union( [ filter, this.filtersChecked() ] );
1776          request = { tag: [ filter ] };
1777  
1778          // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1779          // or searching the local cache.
1780          this.collection.query( request );
1781      },
1782  
1783      // Clicking on a checkbox to add another filter to the request.
1784      addFilter: function() {
1785          this.filtersChecked();
1786      },
1787  
1788      // Applying filters triggers a tag request.
1789      applyFilters: function( event ) {
1790          var name,
1791              tags = this.filtersChecked(),
1792              request = { tag: tags },
1793              filteringBy = $( '.filtered-by .tags' );
1794  
1795          if ( event ) {
1796              event.preventDefault();
1797          }
1798  
1799          if ( ! tags ) {
1800              wp.a11y.speak( l10n.selectFeatureFilter );
1801              return;
1802          }
1803  
1804          $( 'body' ).addClass( 'filters-applied' );
1805          $( '.filter-links li > a.current' )
1806              .removeClass( 'current' )
1807              .removeAttr( 'aria-current' );
1808  
1809          filteringBy.empty();
1810  
1811          _.each( tags, function( tag ) {
1812              name = $( 'label[for="filter-id-' + tag + '"]' ).text();
1813              filteringBy.append( '<span class="tag">' + name + '</span>' );
1814          });
1815  
1816          // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1817          // or searching the local cache.
1818          this.collection.query( request );
1819      },
1820  
1821      // Save the user's WordPress.org username and get his favorite themes.
1822      saveUsername: function ( event ) {
1823          var username = $( '#wporg-username-input' ).val(),
1824              nonce = $( '#wporg-username-nonce' ).val(),
1825              request = { browse: 'favorites', user: username },
1826              that = this;
1827  
1828          if ( event ) {
1829              event.preventDefault();
1830          }
1831  
1832          // Save username on enter.
1833          if ( event.type === 'keyup' && event.which !== 13 ) {
1834              return;
1835          }
1836  
1837          return wp.ajax.send( 'save-wporg-username', {
1838              data: {
1839                  _wpnonce: nonce,
1840                  username: username
1841              },
1842              success: function () {
1843                  // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1844                  // or searching the local cache.
1845                  that.collection.query( request );
1846              }
1847          } );
1848      },
1849  
1850      /**
1851       * Get the checked filters.
1852       *
1853       * @return {Array} of tags or false
1854       */
1855      filtersChecked: function() {
1856          var items = $( '.filter-group' ).find( ':checkbox' ),
1857              tags = [];
1858  
1859          _.each( items.filter( ':checked' ), function( item ) {
1860              tags.push( $( item ).prop( 'value' ) );
1861          });
1862  
1863          // When no filters are checked, restore initial state and return.
1864          if ( tags.length === 0 ) {
1865              $( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
1866              $( '.filter-drawer .clear-filters' ).hide();
1867              $( 'body' ).removeClass( 'filters-applied' );
1868              return false;
1869          }
1870  
1871          $( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
1872          $( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
1873  
1874          return tags;
1875      },
1876  
1877      activeClass: 'current',
1878  
1879      /**
1880       * When users press the "Upload Theme" button, show the upload form in place.
1881       */
1882      uploader: function() {
1883          var uploadViewToggle = $( '.upload-view-toggle' ),
1884              $body = $( document.body );
1885  
1886          uploadViewToggle.on( 'click', function() {
1887              // Toggle the upload view.
1888              $body.toggleClass( 'show-upload-view' );
1889              // Toggle the `aria-expanded` button attribute.
1890              uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
1891          });
1892      },
1893  
1894      // Toggle the full filters navigation.
1895      moreFilters: function( event ) {
1896          var $body = $( 'body' ),
1897              $toggleButton = $( '.drawer-toggle' );
1898  
1899          event.preventDefault();
1900  
1901          if ( $body.hasClass( 'filters-applied' ) ) {
1902              return this.backToFilters();
1903          }
1904  
1905          this.clearSearch();
1906  
1907          themes.router.navigate( themes.router.baseUrl( '' ) );
1908          // Toggle the feature filters view.
1909          $body.toggleClass( 'show-filters' );
1910          // Toggle the `aria-expanded` button attribute.
1911          $toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
1912      },
1913  
1914      /**
1915       * Clears all the checked filters.
1916       *
1917       * @uses filtersChecked()
1918       */
1919      clearFilters: function( event ) {
1920          var items = $( '.filter-group' ).find( ':checkbox' ),
1921              self = this;
1922  
1923          event.preventDefault();
1924  
1925          _.each( items.filter( ':checked' ), function( item ) {
1926              $( item ).prop( 'checked', false );
1927              return self.filtersChecked();
1928          });
1929      },
1930  
1931      backToFilters: function( event ) {
1932          if ( event ) {
1933              event.preventDefault();
1934          }
1935  
1936          $( 'body' ).removeClass( 'filters-applied' );
1937      },
1938  
1939      clearSearch: function() {
1940          $( '#wp-filter-search-input').val( '' );
1941      }
1942  });
1943  
1944  themes.InstallerRouter = Backbone.Router.extend({
1945      routes: {
1946          'theme-install.php?theme=:slug': 'preview',
1947          'theme-install.php?browse=:sort': 'sort',
1948          'theme-install.php?search=:query': 'search',
1949          'theme-install.php': 'sort'
1950      },
1951  
1952      baseUrl: function( url ) {
1953          return 'theme-install.php' + url;
1954      },
1955  
1956      themePath: '?theme=',
1957      browsePath: '?browse=',
1958      searchPath: '?search=',
1959  
1960      search: function( query ) {
1961          $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
1962      },
1963  
1964      navigate: navigateRouter
1965  });
1966  
1967  
1968  themes.RunInstaller = {
1969  
1970      init: function() {
1971          // Set up the view.
1972          // Passes the default 'section' as an option.
1973          this.view = new themes.view.Installer({
1974              section: 'popular',
1975              SearchView: themes.view.InstallerSearch
1976          });
1977  
1978          // Render results.
1979          this.render();
1980  
1981          // Start debouncing user searches after Backbone.history.start().
1982          this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
1983      },
1984  
1985      render: function() {
1986  
1987          // Render results.
1988          this.view.render();
1989          this.routes();
1990  
1991          if ( Backbone.History.started ) {
1992              Backbone.history.stop();
1993          }
1994          Backbone.history.start({
1995              root: themes.data.settings.adminUrl,
1996              pushState: true,
1997              hashChange: false
1998          });
1999      },
2000  
2001      routes: function() {
2002          var self = this,
2003              request = {};
2004  
2005          // Bind to our global `wp.themes` object
2006          // so that the router is available to sub-views.
2007          themes.router = new themes.InstallerRouter();
2008  
2009          // Handles `theme` route event.
2010          // Queries the API for the passed theme slug.
2011          themes.router.on( 'route:preview', function( slug ) {
2012  
2013              // Remove existing handlers.
2014              if ( themes.preview ) {
2015                  themes.preview.undelegateEvents();
2016                  themes.preview.unbind();
2017              }
2018  
2019              // If the theme preview is active, set the current theme.
2020              if ( self.view.view.theme && self.view.view.theme.preview ) {
2021                  self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } );
2022                  self.view.view.theme.preview();
2023              } else {
2024  
2025                  // Select the theme by slug.
2026                  request.theme = slug;
2027                  self.view.collection.query( request );
2028                  self.view.collection.trigger( 'update' );
2029  
2030                  // Open the theme preview.
2031                  self.view.collection.once( 'query:success', function() {
2032                      $( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
2033                  });
2034  
2035              }
2036          });
2037  
2038          /*
2039           * Handles sorting / browsing routes.
2040           * Also handles the root URL triggering a sort request
2041           * for `popular`, the default view.
2042           */
2043          themes.router.on( 'route:sort', function( sort ) {
2044              if ( ! sort ) {
2045                  sort = 'popular';
2046                  themes.router.navigate( themes.router.baseUrl( '?browse=popular' ), { replace: true } );
2047              }
2048              self.view.sort( sort );
2049  
2050              // Close the preview if open.
2051              if ( themes.preview ) {
2052                  themes.preview.close();
2053              }
2054          });
2055  
2056          // The `search` route event. The router populates the input field.
2057          themes.router.on( 'route:search', function() {
2058              $( '.wp-filter-search' ).trigger( 'focus' ).trigger( 'keyup' );
2059          });
2060  
2061          this.extraRoutes();
2062      },
2063  
2064      extraRoutes: function() {
2065          return false;
2066      }
2067  };
2068  
2069  // Ready...
2070  $( function() {
2071      if ( themes.isInstall ) {
2072          themes.RunInstaller.init();
2073      } else {
2074          themes.Run.init();
2075      }
2076  
2077      // Update the return param just in time.
2078      $( document.body ).on( 'click', '.load-customize', function() {
2079          var link = $( this ), urlParser = document.createElement( 'a' );
2080          urlParser.href = link.prop( 'href' );
2081          urlParser.search = $.param( _.extend(
2082              wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ),
2083              {
2084                  'return': window.location.href
2085              }
2086          ) );
2087          link.prop( 'href', urlParser.href );
2088      });
2089  
2090      $( '.broken-themes .delete-theme' ).on( 'click', function() {
2091          return confirm( _wpThemeSettings.settings.confirmDelete );
2092      });
2093  });
2094  
2095  })( jQuery );
2096  
2097  // Align theme browser thickbox.
2098  jQuery( function($) {
2099      window.tb_position = function() {
2100          var tbWindow = $('#TB_window'),
2101              width = $(window).width(),
2102              H = $(window).height(),
2103              W = ( 1040 < width ) ? 1040 : width,
2104              adminbar_height = 0;
2105  
2106          if ( $('#wpadminbar').length ) {
2107              adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
2108          }
2109  
2110          if ( tbWindow.length >= 1 ) {
2111              tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
2112              $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
2113              tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
2114              if ( typeof document.body.style.maxWidth !== 'undefined' ) {
2115                  tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
2116              }
2117          }
2118      };
2119  
2120      $(window).on( 'resize', function(){ tb_position(); });
2121  });


Generated : Tue Dec 3 08:20:01 2024 Cross-referenced by PHPXref