Make WordPress Core


Ignore:
Timestamp:
09/29/2017 08:12:19 PM (7 years ago)
Author:
westonruter
Message:

Customize: Introduce a new experience for discovering, installing, and previewing themes within the customizer.

Unify the theme-browsing and theme-customization experiences by introducing a comprehensive theme browser and installer directly accessible in the customizer. Replaces the customizer theme switcher with a full-screen panel for discovering/browsing and installing themes available on WordPress.org. Themes can now be installed and previewed directly in the customizer without entering the wp-admin context. Also includes an extensible framework for browsing and installing themes from other sources.

Also includes CSS auto-prefixing added via grunt precommit:css.

For details, see: https://make.wordpress.org/core/2016/10/03/feature-proposal-a-new-experience-for-discovering-installing-and-previewing-themes-in-the-customizer/

Previously [38813] but reverted in [39140].
Fixes #37661, #34843, #38666.
Props celloexpressions, folletto, westonruter, karmatosed, melchoyce, afercia.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/js/customize-controls.js

    r41626 r41648  
    11881188            section.containerParent = api.ensure( section.containerParent );
    11891189
    1190             // Watch for changes to the panel state
     1190            // Watch for changes to the panel state.
    11911191            inject = function ( panelId ) {
    11921192                var parentContainer;
    11931193                if ( panelId ) {
    1194                     // The panel has been supplied, so wait until the panel object is registered
     1194                    // The panel has been supplied, so wait until the panel object is registered.
    11951195                    api.panel( panelId, function ( panel ) {
    1196                         // The panel has been registered, wait for it to become ready/initialized
     1196                        // The panel has been registered, wait for it to become ready/initialized.
    11971197                        panel.deferred.embedded.done( function () {
    11981198                            parentContainer = panel.contentContainer;
     
    12191219            };
    12201220            section.panel.bind( inject );
    1221             inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
     1221            inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
    12221222        },
    12231223
     
    13941394     * wp.customize.ThemesSection
    13951395     *
    1396      * Custom section for themes that functions similarly to a backwards panel,
    1397      * and also handles the theme-details view rendering and navigation.
     1396     * Custom section for themes that loads themes by category, and also
     1397     * handles the theme-details view rendering and navigation.
    13981398     *
    13991399     * @constructor
     
    14061406        template: '',
    14071407        screenshotQueue: null,
    1408         $window: $( window ),
    1409 
    1410         /**
     1408        $window: null,
     1409        $body: null,
     1410        loaded: 0,
     1411        loading: false,
     1412        fullyLoaded: false,
     1413        term: '',
     1414        tags: '',
     1415        nextTerm: '',
     1416        nextTags: '',
     1417        filtersHeight: 0,
     1418        headerContainer: null,
     1419
     1420        /**
     1421         * Initialize.
     1422         *
     1423         * @since 4.9.0
     1424         *
     1425         * @param {string} id - ID.
     1426         * @param {object} options - Options.
     1427         * @returns {void}
     1428         */
     1429        initialize: function( id, options ) {
     1430            var section = this;
     1431            section.headerContainer = $();
     1432            section.$window = $( window );
     1433            section.$body = $( document.body );
     1434            api.Section.prototype.initialize.call( section, id, options );
     1435        },
     1436
     1437        /**
     1438         * Embed the section in the DOM when the themes panel is ready.
     1439         *
     1440         * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
     1441         *
     1442         * @since 4.9.0
     1443         */
     1444        embed: function() {
     1445            var inject,
     1446                section = this;
     1447
     1448            // Watch for changes to the panel state
     1449            inject = function( panelId ) {
     1450                var parentContainer;
     1451                api.panel( panelId, function( panel ) {
     1452
     1453                    // The panel has been registered, wait for it to become ready/initialized
     1454                    panel.deferred.embedded.done( function() {
     1455                        parentContainer = panel.contentContainer;
     1456                        if ( ! section.headContainer.parent().is( parentContainer ) ) {
     1457                            parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
     1458                        }
     1459                        if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
     1460                            section.containerParent.append( section.contentContainer );
     1461                        }
     1462                        section.deferred.embedded.resolve();
     1463                    });
     1464                } );
     1465            };
     1466            section.panel.bind( inject );
     1467            inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
     1468        },
     1469
     1470        /**
     1471         * Set up.
     1472         *
    14111473         * @since 4.2.0
    1412          */
    1413         initialize: function () {
    1414             this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
    1415             return api.Section.prototype.initialize.apply( this, arguments );
    1416         },
    1417 
    1418         /**
    1419          * @since 4.2.0
    1420          */
    1421         ready: function () {
     1474         *
     1475         * @returns {void}
     1476         */
     1477        ready: function() {
    14221478            var section = this;
    14231479            section.overlay = section.container.find( '.theme-overlay' );
     
    14421498                // Pressing the escape key fires a theme:collapse event
    14431499                if ( 27 === event.keyCode ) {
    1444                     section.closeDetails();
     1500                    if ( section.$body.hasClass( 'modal-open' ) ) {
     1501
     1502                        // Escape from the details modal.
     1503                        section.closeDetails();
     1504                    } else {
     1505
     1506                        // Escape from the inifinite scroll list.
     1507                        section.headerContainer.find( '.customize-themes-section-title' ).focus();
     1508                    }
    14451509                    event.stopPropagation(); // Prevent section from being collapsed.
    14461510                }
    14471511            });
    14481512
    1449             _.bindAll( this, 'renderScreenshots' );
     1513            _.bindAll( this, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
    14501514        },
    14511515
     
    14541518         *
    14551519         * Ignore the active states' of the contained theme controls, and just
    1456          * use the section's own active state instead. This ensures empty search
    1457          * results for themes to cause the section to become inactive.
     1520         * use the section's own active state instead. This prevents empty search
     1521         * results for theme sections from causing the section to become inactive.
    14581522         *
    14591523         * @since 4.2.0
     
    14661530
    14671531        /**
     1532         * Attach events.
     1533         *
    14681534         * @since 4.2.0
     1535         *
     1536         * @returns {void}
    14691537         */
    14701538        attachEvents: function () {
    1471             var section = this;
     1539            var section = this, debounced;
    14721540
    14731541            // Expand/Collapse accordion sections on click.
     
    14801548            });
    14811549
    1482             // Expand/Collapse section/panel.
    1483             section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
    1484                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1485                     return;
    1486                 }
    1487                 event.preventDefault(); // Keep this AFTER the key filter above
    1488 
    1489                 if ( section.expanded() ) {
    1490                     section.collapse();
    1491                 } else {
     1550            section.headerContainer = $( '#accordion-section-' + section.id );
     1551
     1552            // Expand section/panel. Only collapse when opening another section.
     1553            section.headerContainer.on( 'click', '.customize-themes-section-title', function() {
     1554
     1555                // Toggle accordion filters under section headers.
     1556                if ( section.headerContainer.find( '.filter-details' ).length ) {
     1557                    section.headerContainer.find( '.customize-themes-section-title' )
     1558                        .toggleClass( 'details-open' )
     1559                        .attr( 'aria-expanded', function( i, attr ) {
     1560                            return 'true' === attr ? 'false' : 'true';
     1561                        });
     1562                    section.headerContainer.find( '.filter-details' ).slideToggle( 180 );
     1563                }
     1564
     1565                // Open the section.
     1566                if ( ! section.expanded() ) {
    14921567                    section.expand();
    14931568                }
    14941569            });
    14951570
     1571            // Preview installed themes.
     1572            section.container.on( 'click', '.theme-actions .preview-theme', function() {
     1573                var themeId = $( this ).data( 'slug' );
     1574
     1575                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     1576                api.panel( 'themes' ).loadThemePreview( themeId ).fail( function() {
     1577                    $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     1578                } );
     1579            });
     1580
    14961581            // Theme navigation in details view.
    1497             section.container.on( 'click keydown', '.left', function( event ) {
    1498                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1499                     return;
    1500                 }
    1501 
    1502                 event.preventDefault(); // Keep this AFTER the key filter above
    1503 
     1582            section.container.on( 'click', '.left', function() {
    15041583                section.previousTheme();
    15051584            });
    15061585
    1507             section.container.on( 'click keydown', '.right', function( event ) {
    1508                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1509                     return;
    1510                 }
    1511 
    1512                 event.preventDefault(); // Keep this AFTER the key filter above
    1513 
     1586            section.container.on( 'click', '.right', function() {
    15141587                section.nextTheme();
    15151588            });
    15161589
    1517             section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
    1518                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1519                     return;
    1520                 }
    1521 
    1522                 event.preventDefault(); // Keep this AFTER the key filter above
    1523 
     1590            section.container.on( 'click', '.theme-backdrop, .close', function() {
    15241591                section.closeDetails();
    15251592            });
    15261593
    1527             var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
    1528             section.container.on( 'input', '#themes-filter', function( event ) {
    1529                 var count,
    1530                     term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
    1531                     controls = section.controls();
    1532 
    1533                 _.each( controls, function( control ) {
    1534                     control.filter( term );
     1594            // Filter-search all theme objects loaded in the section.
     1595            section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
     1596                    section.filterSearch( event.currentTarget );
     1597            });
     1598
     1599            // Event listeners for remote wporg queries with user-entered terms.
     1600            if ( 'wporg' === section.params.action ) {
     1601
     1602                // Search terms.
     1603                debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
     1604                section.contentContainer.on( 'input', '#wp-filter-search-input', function() {
     1605                    debounced( section );
     1606                    if ( ! section.expanded() ) {
     1607                        section.expand();
     1608                    }
     1609                    section.checkTerm( section );
    15351610                });
    15361611
    1537                 renderScreenshots();
    1538 
    1539                 // Update theme count.
    1540                 count = section.container.find( 'li.customize-control:visible' ).length;
    1541                 section.container.find( '.theme-count' ).text( count );
    1542             });
    1543 
    1544             // Pre-load the first 3 theme screenshots.
    1545             api.bind( 'ready', function () {
    1546                 _.each( section.controls().slice( 0, 3 ), function ( control ) {
    1547                     var img, src = control.params.theme.screenshot[0];
    1548                     if ( src ) {
    1549                         img = new Image();
    1550                         img.src = src;
     1612                // Feature filters.
     1613                section.contentContainer.on( 'click', '.filter-group input', function() {
     1614                    section.filtersChecked();
     1615                    section.checkTerm( section );
     1616                });
     1617
     1618                // Toggle feature filter sections.
     1619                section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
     1620                    $( e.currentTarget )
     1621                        .toggleClass( 'open' )
     1622                        .attr( 'aria-expanded', function( i, attr ) {
     1623                            return 'true' === attr ? 'false' : 'true';
     1624                        })
     1625                        .next( '.filter-drawer' ).slideToggle( 180, 'linear', function() {
     1626                            if ( 0 === section.filtersHeight ) {
     1627                                section.filtersHeight = $( this ).height();
     1628
     1629                                // First time, so it's opened.
     1630                                section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
     1631                            }
     1632                        });
     1633                    if ( $( e.currentTarget ).hasClass( 'open' ) ) {
     1634                        section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
     1635                    } else {
     1636                        section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
    15511637                    }
    15521638                });
     1639            }
     1640
     1641            // Setup section cross-linking.
     1642            section.contentContainer.on( 'click', '.no-themes-local .search-dotorg-themes', function() {
     1643                api.section( 'wporg_themes' ).focus();
     1644            });
     1645
     1646            // Move section controls to the themes area.
     1647            api.bind( 'ready', function () {
     1648                section.contentContainer = section.container.find( '.customize-themes-section' );
     1649                section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
     1650                section.container.add( section.headerContainer );
    15531651            });
    15541652        },
     
    15621660         * @param {Object}   args
    15631661         * @param {Boolean}  args.unchanged
    1564          * @param {Callback} args.completeCallback
     1662         * @param {Function} args.completeCallback
     1663         * @returns {void}
    15651664         */
    15661665        onChangeExpanded: function ( expanded, args ) {
     1666
     1667            // Note: there is a second argument 'args' passed
     1668            var section = this,
     1669                container = section.contentContainer.closest( '.customize-themes-full-container' );
    15671670
    15681671            // Immediately call the complete callback if there were no changes
     
    15741677            }
    15751678
    1576             // Note: there is a second argument 'args' passed
    1577             var panel = this,
    1578                 section = panel.contentContainer,
    1579                 overlay = section.closest( '.wp-full-overlay' ),
    1580                 container = section.closest( '.wp-full-overlay-sidebar-content' ),
    1581                 customizeBtn = section.find( '.customize-theme' ),
    1582                 changeBtn = panel.headContainer.find( '.change-theme' );
    1583 
    1584             if ( expanded && ! section.hasClass( 'current-panel' ) ) {
     1679            if ( expanded ) {
     1680
     1681                // Try to load controls if none are loaded yet.
     1682                if ( 0 === section.loaded ) {
     1683                    section.loadControls();
     1684                }
     1685
    15851686                // Collapse any sibling sections/panels
    15861687                api.section.each( function ( otherSection ) {
    1587                     if ( otherSection !== panel ) {
     1688                    var searchTerm;
     1689
     1690                    if ( otherSection !== section ) {
     1691
     1692                        // Try to sync the current search term to the new section.
     1693                        if ( 'themes' === otherSection.params.type ) {
     1694                            searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val();
     1695                            section.contentContainer.find( '.wp-filter-search' ).val( searchTerm );
     1696
     1697                            // Directly initialize an empty remote search to avoid a race condition.
     1698                            if ( '' === searchTerm && '' !== section.term && 'installed' !== section.params.action ) {
     1699                                section.term = '';
     1700                                section.initializeNewQuery( section.term, section.tags );
     1701                            } else {
     1702                                section.checkTerm( section );
     1703                            }
     1704                            section.filterSearch( section.contentContainer.find( '.wp-filter-search' ).get( 0 ) );
     1705                        }
    15881706                        otherSection.collapse( { duration: args.duration } );
    15891707                    }
    15901708                });
    1591                 api.panel.each( function ( otherPanel ) {
    1592                     otherPanel.collapse( { duration: 0 } );
     1709
     1710                section.contentContainer.addClass( 'current-section' );
     1711                container.scrollTop();
     1712                section.headerContainer.find( '.customize-themes-section-title' ).addClass( 'selected' ).attr( 'aria-expanded', 'true' );
     1713
     1714                container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
     1715                container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
     1716
     1717                if ( args.completeCallback ) {
     1718                    args.completeCallback();
     1719                }
     1720                section.updateCount(); // Show this section's count.
     1721            } else {
     1722                section.contentContainer.removeClass( 'current-section' );
     1723
     1724                // Always hide, even if they don't exist or are already hidden.
     1725                section.headerContainer.find( '.customize-themes-section-title' ).removeClass( 'selected details-open' ).attr( 'aria-expanded', 'false' );
     1726                section.headerContainer.find( '.filter-details' ).slideUp( 180 );
     1727
     1728                container.off( 'scroll' );
     1729
     1730                if ( args.completeCallback ) {
     1731                    args.completeCallback();
     1732                }
     1733            }
     1734        },
     1735
     1736        /**
     1737         * Return the section's content element without detaching from the parent.
     1738         *
     1739         * @since 4.9.0
     1740         *
     1741         * @returns {jQuery}
     1742         */
     1743        getContent: function() {
     1744            return this.container.find( '.control-section-content' );
     1745        },
     1746
     1747        /**
     1748         * Load theme data via Ajax and add themes to the section as controls.
     1749         *
     1750         * @since 4.9.0
     1751         *
     1752         * @returns {void}
     1753         */
     1754        loadControls: function() {
     1755            var section = this, params, page, request;
     1756
     1757            if ( section.loading ) {
     1758                return; // We're already loading a batch of themes.
     1759            }
     1760
     1761            // Parameters for every API query. Additional params are set in PHP.
     1762            page = Math.ceil( section.loaded / 100 ) + 1;
     1763            params = {
     1764                'switch-themes-nonce': api.settings.nonce['switch-themes'],
     1765                'wp_customize': 'on',
     1766                'theme_action': section.params.action,
     1767                'customized_theme': api.settings.theme.stylesheet,
     1768                'page': page
     1769            };
     1770
     1771            // Add fields for wporg actions.
     1772            if ( 'wporg' === section.params.action ) {
     1773                params.search = section.term;
     1774                params.tags = section.tags;
     1775            }
     1776
     1777            // Load themes.
     1778            section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
     1779            section.loading = true;
     1780            section.container.find( '.no-themes' ).hide();
     1781            request = wp.ajax.post( 'customize-load-themes', params );
     1782            request.done(function( data ) {
     1783                var themes = data.themes, themeControl, newThemeControls;
     1784
     1785                // Stop and try again if the term changed while loading.
     1786                if ( '' !== section.nextTerm || '' !== section.nextTags ) {
     1787                    if ( section.nextTerm ) {
     1788                        section.term = section.nextTerm;
     1789                    }
     1790                    if ( section.nextTags ) {
     1791                        section.tags = section.nextTags;
     1792                    }
     1793                    section.nextTerm = '';
     1794                    section.nextTags = '';
     1795                    section.loading = false;
     1796                    section.loadControls();
     1797                    return;
     1798                }
     1799
     1800                if ( 0 !== themes.length ) {
     1801                    newThemeControls = [];
     1802
     1803                    // Add controls for each theme.
     1804                    _.each( themes, function( theme ) {
     1805                        var customizeId = section.params.action + '_theme_' + theme.id;
     1806                        themeControl = new api.controlConstructor.theme( customizeId, {
     1807                            params: {
     1808                                type: 'theme',
     1809                                content: '<li id="customize-control-theme-' + section.params.action + '_' + theme.id + '" class="customize-control customize-control-theme"></li>',
     1810                                section: section.params.id,
     1811                                active: true,
     1812                                theme: theme,
     1813                                priority: section.loaded + 1
     1814                            },
     1815                            previewer: api.previewer
     1816                        } );
     1817
     1818                        api.control.add( customizeId, themeControl );
     1819                        newThemeControls.push( themeControl );
     1820                        section.loaded = section.loaded + 1;
     1821                    });
     1822
     1823                    if ( 1 === page ) {
     1824
     1825                        // Pre-load the first 3 theme screenshots.
     1826                        _.each( section.controls().slice( 0, 3 ), function( control ) {
     1827                            var img, src = control.params.theme.screenshot[0];
     1828                            if ( src ) {
     1829                                img = new Image();
     1830                                img.src = src;
     1831                            }
     1832                        });
     1833                        if ( 'installed' !== section.params.action ) {
     1834                            wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
     1835                        }
     1836                    } else {
     1837                        Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
     1838                    }
     1839                    _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
     1840
     1841                    if ( 'installed' === section.params.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
     1842                        section.fullyLoaded = true;
     1843                    }
     1844                } else {
     1845                    if ( 0 === section.loaded ) {
     1846                        section.container.find( '.no-themes' ).show();
     1847                        wp.a11y.speak( section.container.find( '.no-themes' ).text() );
     1848                    } else {
     1849                        section.fullyLoaded = true;
     1850                    }
     1851                }
     1852                if ( 'installed' === section.params.action ) {
     1853                    section.updateCount(); // Count of visible theme controls.
     1854                } else {
     1855                    section.updateCount( data.info.results ); // Total number of results including pages not yet loaded.
     1856                }
     1857                section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
     1858
     1859                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1860                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1861                section.loading = false;
     1862            });
     1863            request.fail(function( data ) {
     1864                if ( 'undefined' === typeof data ) {
     1865                    section.container.find( '.unexpected-error' ).show();
     1866                    wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
     1867                } else if ( 'undefined' !== typeof console && console.error ) {
     1868                    console.error( data );
     1869                }
     1870
     1871                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1872                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1873                section.loading = false;
     1874            });
     1875        },
     1876
     1877        /**
     1878         * Determines whether more themes should be loaded, and loads them.
     1879         *
     1880         * @since 4.9.0
     1881         * @returns {void}
     1882         */
     1883        loadMore: function() {
     1884            var section = this, container, bottom, threshold;
     1885            if ( ! section.fullyLoaded && ! section.loading ) {
     1886                container = section.container.closest( '.customize-themes-full-container' );
     1887
     1888                bottom = container.scrollTop() + container.height();
     1889                threshold = container.prop( 'scrollHeight' ) - 3000; // Use a fixed distance to the bottom of loaded results to avoid unnecessarily loading results sooner when using a percentage of scroll distance.
     1890
     1891                if ( bottom > threshold ) {
     1892                    section.loadControls();
     1893                }
     1894            }
     1895        },
     1896
     1897        /**
     1898         * Event handler for search input that filters visible controls.
     1899         *
     1900         * @since 4.9.0
     1901         *
     1902         * @param {Element} el - The search input element as a raw JS object.
     1903         * @returns {void}
     1904         */
     1905        filterSearch: function( el ) {
     1906            var count = 0,
     1907                visible = false,
     1908                section = this,
     1909                noFilter = ( undefined !== api.section( 'wporg_themes' ) && 'wporg' !== section.params.action ) ? '.no-themes-local' : '.no-themes',
     1910                term = el.value.toLowerCase().trim().replace( '-', ' ' ),
     1911                controls = section.controls(),
     1912                renderScreenshots;
     1913
     1914            if ( section.loading ) {
     1915                return;
     1916            }
     1917
     1918            _.each( controls, function( control ) {
     1919                visible = control.filter( term );
     1920                if ( visible ) {
     1921                    count = count + 1;
     1922                }
     1923            });
     1924
     1925            if ( 0 === count ) {
     1926                section.container.find( noFilter ).show();
     1927                wp.a11y.speak( section.container.find( noFilter ).text() );
     1928            } else {
     1929                section.container.find( noFilter ).hide();
     1930            }
     1931
     1932            renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
     1933
     1934            renderScreenshots();
     1935
     1936            // Update theme count.
     1937            section.updateCount( count );
     1938        },
     1939
     1940        /**
     1941         * Event handler for search input that determines if the terms have changed and loads new controls as needed.
     1942         *
     1943         * @since 4.9.0
     1944         *
     1945         * @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer.
     1946         * @returns {void}
     1947         */
     1948        checkTerm: function( section ) {
     1949            var newTerm;
     1950            if ( 'wporg' === section.params.action ) {
     1951                newTerm = $( '#wp-filter-search-input' ).val();
     1952                if ( section.term !== newTerm ) {
     1953                    section.initializeNewQuery( newTerm, section.tags );
     1954                }
     1955            }
     1956        },
     1957
     1958        /**
     1959         * Check for filters checked in the feature filter list and initialize a new query.
     1960         *
     1961         * @since 4.9.0
     1962         *
     1963         * @returns {void}
     1964         */
     1965        filtersChecked: function() {
     1966            var section = this,
     1967                items = section.container.find( '.filter-group' ).find( ':checkbox' ),
     1968                tags = [];
     1969
     1970            _.each( items.filter( ':checked' ), function( item ) {
     1971                tags.push( $( item ).prop( 'value' ) );
     1972            });
     1973
     1974            // When no filters are checked, restore initial state. Update filter count.
     1975            if ( 0 === tags.length ) {
     1976                tags = '';
     1977                section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show();
     1978                section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide();
     1979            } else {
     1980                section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
     1981                section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide();
     1982                section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show();
     1983            }
     1984
     1985            // Check whether tags have changed, and either load or queue them.
     1986            if ( ! _.isEqual( section.tags, tags ) ) {
     1987                if ( section.loading ) {
     1988                    section.nextTags = tags;
     1989                } else {
     1990                    section.initializeNewQuery( section.term, tags );
     1991                }
     1992            }
     1993        },
     1994
     1995        /**
     1996         * Reset the current query and load new results.
     1997         *
     1998         * @since 4.9.0
     1999         *
     2000         * @param {string} newTerm - New term.
     2001         * @param {Array} newTags - New tags.
     2002         * @returns {void}
     2003         */
     2004        initializeNewQuery: function( newTerm, newTags ) {
     2005            var section = this;
     2006
     2007            // Clear the controls in the section.
     2008            _.each( section.controls(), function( control ) {
     2009                control.container.remove();
     2010                api.control.remove( control.id );
     2011            });
     2012            section.loaded = 0;
     2013            section.fullyLoaded = false;
     2014            section.screenshotQueue = null;
     2015
     2016            // Run a new query, with loadControls handling paging, etc.
     2017            if ( ! section.loading ) {
     2018                section.term = newTerm;
     2019                section.tags = newTags;
     2020                section.loadControls();
     2021            } else {
     2022                section.nextTerm = newTerm; // This will reload from loadControls() with the newest term once the current batch is loaded.
     2023                section.nextTags = newTags; // This will reload from loadControls() with the newest tags once the current batch is loaded.
     2024            }
     2025            if ( ! section.expanded() ) {
     2026                section.expand(); // Expand the section if it isn't expanded.
     2027            }
     2028        },
     2029
     2030        /**
     2031         * Render control's screenshot if the control comes into view.
     2032         *
     2033         * @since 4.2.0
     2034         *
     2035         * @returns {void}
     2036         */
     2037        renderScreenshots: function() {
     2038            var section = this;
     2039
     2040            // Fill queue initially, or check for more if empty.
     2041            if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) {
     2042
     2043                // Add controls that haven't had their screenshots rendered.
     2044                section.screenshotQueue = _.filter( section.controls(), function( control ) {
     2045                    return ! control.screenshotRendered;
    15932046                });
    1594 
    1595                 panel._animateChangeExpanded( function() {
    1596                     changeBtn.attr( 'tabindex', '-1' );
    1597                     customizeBtn.attr( 'tabindex', '0' );
    1598 
    1599                     customizeBtn.focus();
    1600                     section.css( 'top', '' );
    1601                     container.scrollTop( 0 );
    1602 
    1603                     if ( args.completeCallback ) {
    1604                         args.completeCallback();
    1605                     }
    1606                 } );
    1607 
    1608                 overlay.addClass( 'in-themes-panel' );
    1609                 section.addClass( 'current-panel' );
    1610                 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
    1611                 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
    1612 
    1613             } else if ( ! expanded && section.hasClass( 'current-panel' ) ) {
    1614                 panel._animateChangeExpanded( function() {
    1615                     changeBtn.attr( 'tabindex', '0' );
    1616                     customizeBtn.attr( 'tabindex', '-1' );
    1617 
    1618                     changeBtn.focus();
    1619                     section.css( 'top', '' );
    1620 
    1621                     if ( args.completeCallback ) {
    1622                         args.completeCallback();
    1623                     }
    1624                 } );
    1625 
    1626                 overlay.removeClass( 'in-themes-panel' );
    1627                 section.removeClass( 'current-panel' );
    1628                 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
    1629             }
    1630         },
    1631 
    1632         /**
    1633          * Render control's screenshot if the control comes into view.
    1634          *
    1635          * @since 4.2.0
    1636          */
    1637         renderScreenshots: function( ) {
    1638             var section = this;
    1639 
    1640             // Fill queue initially.
    1641             if ( section.screenshotQueue === null ) {
    1642                 section.screenshotQueue = section.controls();
    1643             }
    1644 
    1645             // Are all screenshots rendered?
     2047            }
     2048
     2049            // Are all screenshots rendered (for now)?
    16462050            if ( ! section.screenshotQueue.length ) {
    16472051                return;
     
    16792083
    16802084        /**
     2085         * Get visible count.
     2086         *
     2087         * @since 4.9.0
     2088         *
     2089         * @returns {int} Visible count.
     2090         */
     2091        getVisibleCount: function() {
     2092            return this.contentContainer.find( 'li.customize-control:visible' ).length;
     2093        },
     2094
     2095        /**
     2096         * Update the number of themes in the section.
     2097         *
     2098         * @since 4.9.0
     2099         *
     2100         * @returns {void}
     2101         */
     2102        updateCount: function( count ) {
     2103            var section = this, countEl, displayed;
     2104
     2105            if ( ! count && 0 !== count ) {
     2106                count = section.getVisibleCount();
     2107            }
     2108
     2109            displayed = section.contentContainer.find( '.themes-displayed' );
     2110            countEl = section.contentContainer.find( '.theme-count' );
     2111
     2112            if ( 0 === count ) {
     2113                countEl.text( '0' );
     2114            } else {
     2115
     2116                // Animate the count change for emphasis.
     2117                displayed.fadeOut( 180, function() {
     2118                    countEl.text( count );
     2119                    displayed.fadeIn( 180 );
     2120                } );
     2121                wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
     2122            }
     2123        },
     2124
     2125        /**
    16812126         * Advance the modal to the next theme.
    16822127         *
    16832128         * @since 4.2.0
     2129         *
     2130         * @returns {void}
    16842131         */
    16852132        nextTheme: function () {
     
    16962143         *
    16972144         * @since 4.2.0
     2145         *
     2146         * @returns {object|boolean} Next theme.
    16982147         */
    16992148        getNextTheme: function () {
    1700             var control, next;
    1701             control = api.control( 'theme_' + this.currentTheme );
     2149            var section = this, control, next;
     2150            control = api.control( section.params.action + '_theme_' + this.currentTheme );
    17022151            next = control.container.next( 'li.customize-control-theme' );
    17032152            if ( ! next.length ) {
    17042153                return false;
    17052154            }
    1706             next = next[0].id.replace( 'customize-control-', '' );
     2155            next = next[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    17072156            control = api.control( next );
    17082157
     
    17142163         *
    17152164         * @since 4.2.0
     2165         * @returns {void}
    17162166         */
    17172167        previousTheme: function () {
     
    17282178         *
    17292179         * @since 4.2.0
     2180         * @returns {object|boolean} Previous theme.
    17302181         */
    17312182        getPreviousTheme: function () {
    1732             var control, previous;
    1733             control = api.control( 'theme_' + this.currentTheme );
     2183            var section = this, control, previous;
     2184            control = api.control( section.params.action + '_theme_' + this.currentTheme );
    17342185            previous = control.container.prev( 'li.customize-control-theme' );
    17352186            if ( ! previous.length ) {
    17362187                return false;
    17372188            }
    1738             previous = previous[0].id.replace( 'customize-control-', '' );
     2189            previous = previous[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    17392190            control = api.control( previous );
    17402191
     
    17462197         *
    17472198         * @since 4.2.0
     2199         *
     2200         * @returns {void}
    17482201         */
    17492202        updateLimits: function () {
     
    18212274         * @since 4.2.0
    18222275         *
    1823          * @param {Object}   theme
     2276         * @param {object} theme - Theme.
     2277         * @param {Function} [callback] - Callback once the details have been shown.
     2278         * @returns {void}
    18242279         */
    18252280        showDetails: function ( theme, callback ) {
    1826             var section = this, link;
    1827             callback = callback || function(){};
     2281            var section = this;
    18282282            section.currentTheme = theme.id;
    18292283            section.overlay.html( section.template( theme ) )
    18302284                .fadeIn( 'fast' )
    18312285                .focus();
    1832             $( 'body' ).addClass( 'modal-open' );
     2286            section.$body.addClass( 'modal-open' );
    18332287            section.containFocus( section.overlay );
    18342288            section.updateLimits();
    1835 
    1836             link = section.overlay.find( '.inactive-theme > a' );
    1837 
    1838             link.on( 'click', function( event ) {
    1839                 event.preventDefault();
    1840 
    1841                 // Short-circuit if request is currently being made.
    1842                 if ( link.hasClass( 'disabled' ) ) {
    1843                     return;
    1844                 }
    1845                 link.addClass( 'disabled' );
    1846 
    1847                 section.loadThemePreview( theme.id ).fail( function() {
    1848                     link.removeClass( 'disabled' );
    1849                 } );
    1850             } );
    1851             callback();
     2289            wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
     2290            if ( callback ) {
     2291                callback();
     2292            }
    18522293        },
    18532294
     
    18562297         *
    18572298         * @since 4.2.0
     2299         *
     2300         * @returns {void}
    18582301         */
    18592302        closeDetails: function () {
    1860             $( 'body' ).removeClass( 'modal-open' );
    1861             this.overlay.fadeOut( 'fast' );
    1862             api.control( 'theme_' + this.currentTheme ).focus();
     2303            var section = this;
     2304            section.$body.removeClass( 'modal-open' );
     2305            section.overlay.fadeOut( 'fast' );
     2306            api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
    18632307        },
    18642308
     
    18672311         *
    18682312         * @since 4.2.0
     2313         *
     2314         * @param {jQuery} el - Element to contain focus.
     2315         * @returns {void}
    18692316         */
    18702317        containFocus: function( el ) {
     
    19192366            section.containerParent = '#customize-outer-theme-controls';
    19202367            section.containerPaneParent = '.customize-outer-pane-parent';
    1921             return api.Section.prototype.initialize.apply( section, arguments );
     2368            api.Section.prototype.initialize.apply( section, arguments );
    19222369        },
    19232370
     
    19402387                backBtn = content.find( '.customize-section-back' ),
    19412388                sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
    1942                 body = $( 'body' ),
     2389                body = $( document.body ),
    19432390                expand, panel;
    19442391
     
    20592506            if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
    20602507                container.append( panel.contentContainer );
    2061                 panel.renderContent();
    2062             }
     2508            }
     2509            panel.renderContent();
    20632510
    20642511            panel.deferred.embedded.resolve();
     
    21322579         * @since 4.1.0
    21332580         *
    2134          * @returns {boolean}
     2581         * @returns {boolean} Whether contextually active.
    21352582         */
    21362583        isContextuallyActive: function () {
     
    21472594
    21482595        /**
    2149          * Update UI to reflect expanded state
     2596         * Update UI to reflect expanded state.
    21502597         *
    21512598         * @since 4.1.0
     
    21552602         * @param {Boolean}  args.unchanged
    21562603         * @param {Function} args.completeCallback
     2604         * @returns {void}
    21572605         */
    21582606        onChangeExpanded: function ( expanded, args ) {
     
    22622710                panel.contentContainer.html( template( panel.params ) );
    22632711            }
     2712        }
     2713    });
     2714
     2715    /**
     2716     * Class wp.customize.ThemesPanel.
     2717     *
     2718     * Custom section for themes that displays without the customize preview.
     2719     *
     2720     * @constructor
     2721     * @augments wp.customize.Panel
     2722     * @augments wp.customize.Container
     2723     */
     2724    api.ThemesPanel = api.Panel.extend({
     2725
     2726        /**
     2727         * Initialize.
     2728         *
     2729         * @since 4.9.0
     2730         *
     2731         * @param {string} id - The ID for the panel.
     2732         * @param {object} options - Options.
     2733         * @returns {void}
     2734         */
     2735        initialize: function( id, options ) {
     2736            var panel = this;
     2737            panel.installingThemes = [];
     2738            api.Panel.prototype.initialize.call( panel, id, options );
     2739        },
     2740
     2741        /**
     2742         * Attach events.
     2743         *
     2744         * @since 4.9.0
     2745         * @returns {void}
     2746         */
     2747        attachEvents: function() {
     2748            var panel = this;
     2749
     2750            // Attach regular panel events.
     2751            api.Panel.prototype.attachEvents.apply( panel );
     2752
     2753            // Collapse panel to customize the current theme.
     2754            panel.contentContainer.on( 'click', '.customize-theme', function() {
     2755                panel.collapse();
     2756            });
     2757
     2758            // Toggle between filtering and browsing themes on mobile.
     2759            panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() {
     2760                $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
     2761            });
     2762
     2763            // Install (and maybe preview) a theme.
     2764            panel.contentContainer.on( 'click', '.theme-install', function( event ) {
     2765                panel.installTheme( event );
     2766            });
     2767
     2768            // Update a theme. Theme cards have the class, the details modal has the id.
     2769            panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
     2770
     2771                // #update-theme is a link.
     2772                event.preventDefault();
     2773                event.stopPropagation();
     2774
     2775                panel.updateTheme( event );
     2776            });
     2777
     2778            // Delete a theme.
     2779            panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
     2780                panel.deleteTheme( event );
     2781            });
     2782
     2783            _.bindAll( panel, 'installTheme', 'updateTheme' );
     2784        },
     2785
     2786        /**
     2787         * Update UI to reflect expanded state
     2788         *
     2789         * @since 4.9.0
     2790         *
     2791         * @param {Boolean}  expanded - Expanded state.
     2792         * @param {Object}   args - Args.
     2793         * @param {Boolean}  args.unchanged - Whether or not the state changed.
     2794         * @param {Function} args.completeCallback - Callback to execute when the animation completes.
     2795         * @returns {void}
     2796         */
     2797        onChangeExpanded: function( expanded, args ) {
     2798            var panel = this, overlay;
     2799
     2800            // Expand/collapse the panel normally.
     2801            api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
     2802
     2803            // Immediately call the complete callback if there were no changes
     2804            if ( args.unchanged ) {
     2805                if ( args.completeCallback ) {
     2806                    args.completeCallback();
     2807                }
     2808                return;
     2809            }
     2810
     2811            overlay = panel.headContainer.closest( '.wp-full-overlay' );
     2812
     2813            if ( expanded ) {
     2814                overlay
     2815                    .addClass( 'in-themes-panel' )
     2816                    .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
     2817
     2818                // Automatically open the installed themes section (except on small screens).
     2819                if ( 600 < window.innerWidth ) {
     2820                    api.section( 'installed_themes' ).expand();
     2821                }
     2822            } else {
     2823                overlay
     2824                    .removeClass( 'in-themes-panel' )
     2825                    .find( '.customize-themes-full-container' ).removeClass( 'animate' );
     2826            }
     2827        },
     2828
     2829        /**
     2830         * Install a theme via wp.updates.
     2831         *
     2832         * @since 4.9.0
     2833         *
     2834         * @returns {void}
     2835         */
     2836        installTheme: function( event ) {
     2837            var panel = this, preview = false, slug = $( event.target ).data( 'slug' );
     2838
     2839            if ( _.contains( panel.installingThemes, slug ) ) {
     2840                return; // Theme is already being installed.
     2841            }
     2842
     2843            wp.updates.maybeRequestFilesystemCredentials( event );
     2844
     2845            $( document ).one( 'wp-theme-install-success', function( event, response ) {
     2846                var theme = false, customizeId, themeControl;
     2847                if ( preview ) {
     2848
     2849                    panel.loadThemePreview( slug ).fail( function() {
     2850                        $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     2851                    } );
     2852
     2853                } else {
     2854                    api.control.each( function( control ) {
     2855                        if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2856                            theme = control.params.theme; // Used below to add theme control.
     2857                            control.rerenderAsInstalled( true );
     2858                        }
     2859                    });
     2860
     2861                    // Don't add the same theme more than once.
     2862                    if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) {
     2863                        return;
     2864                    }
     2865
     2866                    // Add theme control to installed section.
     2867                    theme.type = 'installed';
     2868                    customizeId = 'installed_theme_' + theme.id;
     2869                    themeControl = new api.controlConstructor.theme( customizeId, {
     2870                        params: {
     2871                            type: 'theme',
     2872                            content: $( '<li class="customize-control customize-control-theme"></li>' ).attr( 'id', 'customize-control-theme-installed_' + theme.id ).prop( 'outerHTML' ),
     2873                            section: 'installed_themes',
     2874                            active: true,
     2875                            theme: theme,
     2876                            priority: 0 // Add all newly-installed themes to the top.
     2877                        },
     2878                        previewer: api.previewer
     2879                    } );
     2880
     2881                    api.control.add( customizeId, themeControl );
     2882                    api.control( customizeId ).container.trigger( 'render-screenshot' );
     2883
     2884                    // Close the details modal if it's open to the installed theme.
     2885                    api.section.each( function( section ) {
     2886                        if ( 'themes' === section.params.type ) {
     2887                            if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
     2888                                section.closeDetails();
     2889                            }
     2890                        }
     2891                    });
     2892                }
     2893            } );
     2894
     2895            panel.installingThemes.push( $( event.target ).data( 'slug' ) ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
     2896            wp.updates.installTheme( {
     2897                slug: slug
     2898            } );
     2899
     2900            // Also preview the theme as the event is triggered on Install & Preview.
     2901            if ( $( event.target ).hasClass( 'preview' ) ) {
     2902                preview = true;
     2903                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     2904                wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text-installing-theme' ).text() );
     2905            }
     2906        },
     2907
     2908        /**
     2909         * Load theme preview.
     2910         *
     2911         * @since 4.9.0
     2912         *
     2913         * @param {string} themeId Theme ID.
     2914         * @returns {jQuery.promise} Promise.
     2915         */
     2916        loadThemePreview: function( themeId ) {
     2917            var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
     2918
     2919            urlParser = document.createElement( 'a' );
     2920            urlParser.href = location.href;
     2921            urlParser.search = $.param( _.extend(
     2922                api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
     2923                {
     2924                    theme: themeId,
     2925                    changeset_uuid: api.settings.changeset.uuid
     2926                }
     2927            ) );
     2928
     2929            // Update loading message. Everything else is handled by reloading the page.
     2930            $( '#customize-themes-loading-container span' ).hide();
     2931            $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' );
     2932            wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text' ).text() );
     2933            overlay = $( '.wp-full-overlay' );
     2934            overlay.addClass( 'customize-loading' );
     2935
     2936            onceProcessingComplete = function() {
     2937                var request;
     2938                if ( api.state( 'processing' ).get() > 0 ) {
     2939                    return;
     2940                }
     2941
     2942                api.state( 'processing' ).unbind( onceProcessingComplete );
     2943
     2944                request = api.requestChangesetUpdate();
     2945                request.done( function() {
     2946                    deferred.resolve();
     2947                    $( window ).off( 'beforeunload.customize-confirm' );
     2948                    window.location.href = urlParser.href;
     2949                } );
     2950                request.fail( function() {
     2951                    overlay.removeClass( 'customize-loading' );
     2952                    deferred.reject();
     2953                } );
     2954            };
     2955
     2956            if ( 0 === api.state( 'processing' ).get() ) {
     2957                onceProcessingComplete();
     2958            } else {
     2959                api.state( 'processing' ).bind( onceProcessingComplete );
     2960            }
     2961
     2962            return deferred.promise();
     2963        },
     2964
     2965        /**
     2966         * Update a theme via wp.updates.
     2967         *
     2968         * @since 4.9.0
     2969         *
     2970         * @param {jQuery.Event} event - Event.
     2971         * @returns {void}
     2972         */
     2973        updateTheme: function( event ) {
     2974            wp.updates.maybeRequestFilesystemCredentials( event );
     2975
     2976            $( document ).one( 'wp-theme-update-success', function( e, response ) {
     2977
     2978                // Rerender the control to reflect the update.
     2979                api.control.each( function( control ) {
     2980                    if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2981                        control.params.theme.hasUpdate = false;
     2982                        control.rerenderAsInstalled( true );
     2983                    }
     2984                });
     2985            } );
     2986
     2987            wp.updates.updateTheme( {
     2988                slug: $( event.target ).closest( '.notice' ).data( 'slug' )
     2989            } );
     2990        },
     2991
     2992        /**
     2993         * Delete a theme via wp.updates.
     2994         *
     2995         * @since 4.9.0
     2996         *
     2997         * @param {jQuery.Event} event - Event.
     2998         * @returns {void}
     2999         */
     3000        deleteTheme: function( event ) {
     3001            var theme, section;
     3002            theme = $( event.target ).data( 'slug' );
     3003            section = api.section( 'installed_themes' );
     3004
     3005            event.preventDefault();
     3006
     3007            // Confirmation dialog for deleting a theme.
     3008            if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
     3009                return;
     3010            }
     3011
     3012            wp.updates.maybeRequestFilesystemCredentials( event );
     3013
     3014            $( document ).one( 'wp-theme-delete-success', function() {
     3015                var control = api.control( 'installed_theme_' + theme );
     3016
     3017                // Remove theme control.
     3018                control.container.remove();
     3019                api.control.remove( control.id );
     3020
     3021                // Update installed count.
     3022                section.loaded = section.loaded - 1;
     3023                section.updateCount();
     3024
     3025                // Rerender any other theme controls as uninstalled.
     3026                api.control.each( function( control ) {
     3027                    if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
     3028                        control.rerenderAsInstalled( false );
     3029                    }
     3030                });
     3031            } );
     3032
     3033            wp.updates.deleteTheme( {
     3034                slug: theme
     3035            } );
     3036
     3037            // Close modal and focus the section.
     3038            section.closeDetails();
     3039            section.focus();
    22643040        }
    22653041    });
     
    26143390         * @param {Object}   args
    26153391         * @param {Number}   args.duration
    2616          * @param {Callback} args.completeCallback
     3392         * @param {Function} args.completeCallback
    26173393         */
    26183394        onChangeActive: function ( active, args ) {
     
    37864562
    37874563        touchDrag: false,
    3788         isRendered: false,
    3789 
    3790         /**
    3791          * Defer rendering the theme control until the section is displayed.
    3792          *
    3793          * @since 4.2.0
    3794          */
    3795         renderContent: function () {
    3796             var control = this,
    3797                 renderContentArgs = arguments;
    3798 
    3799             api.section( control.section(), function( section ) {
    3800                 if ( section.expanded() ) {
    3801                     api.Control.prototype.renderContent.apply( control, renderContentArgs );
    3802                     control.isRendered = true;
    3803                 } else {
    3804                     section.expanded.bind( function( expanded ) {
    3805                         if ( expanded && ! control.isRendered ) {
    3806                             api.Control.prototype.renderContent.apply( control, renderContentArgs );
    3807                             control.isRendered = true;
    3808                         }
    3809                     } );
    3810                 }
    3811             } );
    3812         },
     4564        screenshotRendered: false,
    38134565
    38144566        /**
     
    38344586
    38354587                // Prevent the modal from showing when the user clicks the action button.
    3836                 if ( $( event.target ).is( '.theme-actions .button' ) ) {
     4588                if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
    38374589                    return;
    38384590                }
    38394591
    3840                 api.section( control.section() ).loadThemePreview( control.params.theme.id );
    3841             });
    3842 
    3843             control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
    3844                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    3845                     return;
    3846                 }
    3847 
    38484592                event.preventDefault(); // Keep this AFTER the key filter above
    3849 
    38504593                api.section( control.section() ).showDetails( control.params.theme );
    38514594            });
     
    38584601                    $screenshot.attr( 'src', source );
    38594602                }
    3860             });
    3861         },
    3862 
    3863         /**
    3864          * Show or hide the theme based on the presence of the term in the title, description, and author.
     4603                control.screenshotRendered = true;
     4604            });
     4605        },
     4606
     4607        /**
     4608         * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
    38654609         *
    38664610         * @since 4.2.0
     4611         * @returns {boolean} Whether a theme control was activated or not.
    38674612         */
    38684613        filter: function( term ) {
     
    38754620            if ( -1 !== haystack.search( term ) ) {
    38764621                control.activate();
     4622                return true;
    38774623            } else {
    38784624                control.deactivate();
    3879             }
     4625                return false;
     4626            }
     4627        },
     4628
     4629        /**
     4630         * Rerender the theme from its JS template with the installed type.
     4631         *
     4632         * @since 4.9.0
     4633         *
     4634         * @returns {void}
     4635         */
     4636        rerenderAsInstalled: function( installed ) {
     4637            var control = this, section;
     4638            if ( installed ) {
     4639                control.params.theme.type = 'installed';
     4640            } else {
     4641                section = api.section( control.params.section );
     4642                control.params.theme.type = section.params.action;
     4643            }
     4644            control.renderContent(); // Replaces existing content.
     4645            control.container.trigger( 'render-screenshot' );
    38804646        }
    38814647    });
     
    52816047        code_editor:         api.CodeEditorControl
    52826048    };
    5283     api.panelConstructor = {};
     6049    api.panelConstructor = {
     6050        themes: api.ThemesPanel
     6051    };
    52846052    api.sectionConstructor = {
    52856053        themes: api.ThemesSection,
     
    54006168        // Sort the sections within each panel
    54016169        api.panel.each( function ( panel ) {
     6170            if ( 'themes' === panel.id ) {
     6171                return; // Don't reflow theme sections, as doing so moves them after the themes container.
     6172            }
     6173
    54026174            var sections = panel.sections(),
    54036175                sectionHeadContainers = _.pluck( sections, 'headContainer' );
     
    62246996            };
    62256997
    6226             /**
    6227              * Deactivate themes section if changeset status is not auto-draft
    6228              */
    6229             api.section( 'themes', function( section ) {
     6998            // Deactivate themes panel if changeset status is not auto-draft.
     6999            api.panel( 'themes', function( panel ) {
    62307000                var canActivate;
    62317001
     
    62347004                };
    62357005
    6236                 section.active.validate = canActivate;
    6237                 section.active.set( canActivate() );
     7006                panel.active.validate = canActivate;
     7007                panel.active.set( canActivate() );
    62387008                changesetStatus.bind( function() {
    6239                     section.active.set( canActivate() );
     7009                    panel.active.set( canActivate() );
    62407010                } );
    62417011            } );
     
    64017171
    64027172        // Keyboard shortcuts - esc to exit section/panel.
    6403         $( 'body' ).on( 'keydown', function( event ) {
     7173        body.on( 'keydown', function( event ) {
    64047174            var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
    64057175
     
    64417211            collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
    64427212            if ( collapsedObject ) {
     7213                if ( 'themes' === collapsedObject.params.type ) {
     7214
     7215                    // Themes panel or section.
     7216                    if ( body.hasClass( 'modal-open' ) ) {
     7217                        collapsedObject.closeDetails();
     7218                    } else {
     7219
     7220                        // If we're collapsing a section, collapse the panel also.
     7221                        wp.customize.panel( 'themes' ).collapse();
     7222                    }
     7223                    return;
     7224                }
    64437225                collapsedObject.collapse();
    64447226                event.preventDefault();
Note: See TracChangeset for help on using the changeset viewer.