home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-admin / js / theme.js < prev    next >
Encoding:
Text File  |  2017-11-23  |  53.1 KB  |  2,066 lines

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