Make WordPress Core

Changeset 41648


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.

Location:
trunk
Files:
1 added
16 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/common.css

    r41622 r41648  
    124124    border: 0;
    125125    clip: rect(1px, 1px, 1px, 1px);
     126    -webkit-clip-path: inset(50%);
    126127    clip-path: inset(50%);
    127128    height: 1px;
  • trunk/src/wp-admin/css/customize-controls.css

    r41626 r41648  
    590590
    591591#customize-theme-controls .customize-pane-child.open,
    592 #customize-theme-controls .customize-pane-child.current-panel,
    593 #customize-theme-controls .customize-themes-panel.customize-pane-child.current-panel {
     592#customize-theme-controls .customize-pane-child.current-panel {
    594593    -webkit-transform: none;
    595594    transform: none;
    596595}
    597596
    598 #customize-theme-controls .customize-themes-panel.customize-pane-child,
    599597.section-open #customize-theme-controls .customize-pane-parent,
    600598.in-sub-panel #customize-theme-controls .customize-pane-parent,
    601599.section-open #customize-info,
    602600.in-sub-panel #customize-info,
    603 .in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel,
    604 .in-themes-panel #customize-theme-controls .customize-pane-parent,
    605 .in-themes-panel #customize-info {
     601.in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel {
    606602    visibility: hidden;
    607603    height: 0;
     
    613609.section-open #customize-theme-controls .customize-pane-parent.busy,
    614610.in-sub-panel #customize-theme-controls .customize-pane-parent.busy,
    615 .in-themes-panel #customize-theme-controls .customize-pane-parent.busy,
    616611.section-open #customize-info.busy,
    617612.in-sub-panel #customize-info.busy,
    618 .in-themes-panel #customize-info.busy,
    619613.busy.section-open.in-sub-panel #customize-theme-controls .customize-pane-child.current-panel,
    620614#customize-theme-controls .customize-pane-child.open,
     
    624618    height: auto;
    625619    overflow: auto;
    626 }
    627 
    628 .in-themes-panel #customize-theme-controls .customize-pane-parent,
    629 .in-themes-panel #customize-info {
    630     -webkit-transform: translateX(100%);
    631     transform: translateX(100%);
    632620}
    633621
     
    15731561}
    15741562
    1575 /* #customize-container is reused from customize-loader.js, hence the naming. */
    1576 .wp-customizer .customize-loading #customize-container {
     1563.wp-customizer .customize-loading #customize-themes-loading-container {
    15771564    display: block;
    1578     -webkit-animation: customize-reload .75s; /* Can't use `transition` because `display` changes here. */
    1579     animation: customize-reload .75s;
    1580 }
    1581 
    1582 #customize-theme-controls .control-section-themes .accordion-section-title:hover, /* Not a focusable element. */
    1583 #customize-theme-controls .control-section-themes .accordion-section-title {
     1565    -webkit-animation: customize-reload .5s; /* Can't use `transition` because `display` changes here. */
     1566    animation: customize-reload .5s;
     1567}
     1568
     1569.customize-loading #customize-themes-loading-container span {
     1570    clear: both;
     1571    color: #191e23;
     1572    font-size: 18px;
     1573    font-style: normal;
     1574    margin: 0;
     1575    padding: 2em 0;
     1576    text-align: center;
     1577    width: 100%;
     1578    display: block;
     1579    top: 50%;
     1580    position: relative;
     1581}
     1582
     1583.customize-loading #customize-themes-loading-container .customize-loading-text {
     1584    display: none;
     1585}
     1586
     1587#customize-theme-controls .control-panel-themes {
     1588    border-bottom: none;
     1589}
     1590
     1591#customize-theme-controls .control-panel-themes > .accordion-section-title:hover, /* Not a focusable element. */
     1592#customize-theme-controls .control-panel-themes > .accordion-section-title {
    15841593    cursor: default;
    15851594    background: #fff;
     
    15881597    border-bottom: 1px solid #ddd;
    15891598    border-left: none;
    1590     margin-top: 0;
    1591 }
    1592 #customize-theme-controls .control-section-themes .customize-section-back {
    1593     position: absolute;
    1594     right: 0;
    1595     top: 0;
    1596     height: 80px;
    1597     border-left: 1px solid #ddd;
    1598     border-right: 4px solid #fff;
    1599 }
    1600 #customize-theme-controls .control-section-themes .customize-section-back:before {
    1601     content: "\f345";
    1602 }
    1603 #customize-theme-controls .control-section-themes .customize-section-back:hover,
    1604 #customize-theme-controls .control-section-themes .customize-section-back:focus {
    1605     border-right-color: #0073aa;
     1599    border-right: none;
     1600    margin: 0 0 15px 0;
     1601    padding-right: 100px; /* Space for the button */
    16061602}
    16071603
     
    16261622}
    16271623
     1624.control-panel-themes .accordion-section-title span.customize-action,
     1625#customize-controls .customize-section-title span.customize-action,
    16281626#customize-controls .control-section-themes .accordion-section-title span.customize-action,
    16291627#customize-controls .customize-section-title span.customize-action,
     
    16341632}
    16351633
    1636 #customize-controls .control-section-themes .accordion-section-title .change-theme,
    1637 #customize-controls .customize-themes-panel .accordion-section-title .customize-theme {
     1634#customize-theme-controls .control-panel-themes .accordion-section-title .change-theme {
    16381635    position: absolute;
    16391636    right: 10px;
     
    16431640}
    16441641
    1645 #customize-controls .control-section-themes .accordion-section-title:before {
     1642#customize-theme-controls .control-panel-themes > .accordion-section-title:after {
    16461643    display: none;
    16471644}
    16481645
    1649 #customize-controls .customize-themes-panel {
    1650     padding: 0 8px;
    1651     background: #f1f1f1;
     1646.control-panel-themes .customize-themes-full-container {
     1647    position: fixed;
     1648    top: 0;
     1649    left: 0;
     1650    transition: .18s left ease-in-out;
     1651    margin: 46px 0 0 300px;
     1652    padding: 25px 0;
     1653    overflow-y: scroll;
     1654    width: calc(100% - 300px);
     1655    height: calc(100% - 96px);
     1656    background: #eee;
     1657    z-index: 20;
     1658}
     1659
     1660/* Animations for opening the themes panel */
     1661#customize-header-actions .save,
     1662#customize-header-actions .spinner,
     1663#customize-header-actions .customize-controls-preview-toggle {
     1664    position: relative;
     1665    top: 0;
     1666    transition: .18s top ease-in-out;
     1667}
     1668
     1669#customize-footer-actions,
     1670#customize-footer-actions .collapse-sidebar {
     1671    bottom: 0;
     1672    transition: .18s bottom ease-in-out;
     1673}
     1674
     1675.in-themes-panel:not(.animating) #customize-header-actions .save,
     1676.in-themes-panel:not(.animating) #customize-header-actions #publish-settings,
     1677.in-themes-panel:not(.animating) #customize-header-actions .spinner,
     1678.in-themes-panel:not(.animating) #customize-header-actions .customize-controls-preview-toggle,
     1679.in-themes-panel:not(.animating) #customize-preview,
     1680.in-themes-panel:not(.animating) #customize-footer-actions {
     1681    visibility: hidden;
     1682}
     1683
     1684.wp-full-overlay.in-themes-panel {
     1685    background: #eee; /* Prevents a black flash when fading in the panel */
     1686}
     1687
     1688.in-themes-panel #customize-header-actions .save,
     1689.in-themes-panel #customize-header-actions .spinner,
     1690.in-themes-panel #customize-header-actions .customize-controls-preview-toggle {
     1691    top: -45px;
     1692}
     1693
     1694.in-themes-panel #customize-footer-actions,
     1695.in-themes-panel #customize-footer-actions .collapse-sidebar {
     1696    bottom: -45px;
     1697}
     1698
     1699/* Don't show the theme count while the panel opens, as it's in the wrong place during the animation */
     1700.in-themes-panel.animating .control-panel-themes .filter-themes-count {
     1701    display: none;
     1702}
     1703
     1704.in-themes-panel.wp-full-overlay .wp-full-overlay-sidebar-content {
     1705    bottom: 0;
     1706}
     1707
     1708.themes-filter-bar .feature-filter-toggle {
     1709    float: right;
     1710    margin: 3px 0 3px 25px;
     1711}
     1712
     1713.themes-filter-bar .feature-filter-toggle:before {
     1714    content: "\f111";
     1715    margin: 0 5px 0 0;
     1716    font: normal 16px/1 dashicons;
     1717    vertical-align: text-bottom;
     1718    -webkit-font-smoothing: antialiased;
     1719    -moz-osx-font-smoothing: grayscale;
     1720}
     1721
     1722.themes-filter-bar .feature-filter-toggle.open {
     1723    background: #eee;
     1724    border-color: #999;
     1725    box-shadow: inset 0 2px 5px -3px rgba( 0, 0, 0, 0.5 );
     1726    -webkit-transform: translateY(1px);
     1727    transform: translateY(1px);
     1728}
     1729
     1730.themes-filter-bar .feature-filter-toggle .filter-count-filters {
     1731    display: none;
     1732}
     1733
     1734.themes-filter-bar .filter-drawer {
    16521735    box-sizing: border-box;
    1653 }
    1654 
    1655 #customize-controls .customize-themes-panel .accordion-section-title:first-child {
    1656     margin-top: 0;
    1657 }
    1658 
    1659 #customize-controls .customize-themes-panel .accordion-section-title:nth-child(2) {
     1736    width: 100%;
     1737    position: absolute;
     1738    top: 46px;
     1739    left: 0;
     1740    padding: 25px 0 25px 25px;
     1741    border-top: 0;
     1742    margin: 0;
     1743    background: #eee;
     1744    border-bottom: 1px solid #ddd;
     1745}
     1746
     1747.themes-filter-bar .filter-group {
     1748    margin: 0 25px 0 0;
     1749    width: calc( (100% - 75px) / 3);
     1750    min-width: 200px;
     1751    max-width: 320px;
     1752}
     1753
     1754/* Adds a delay before fading in to avoid it "jumping" */
     1755@-webkit-keyframes themes-fade-in {
     1756    0% {
     1757        opacity: 0;
     1758    }
     1759    50% {
     1760        opacity: 0;
     1761    }
     1762    100% {
     1763        opacity: 1;
     1764    }
     1765}
     1766@keyframes themes-fade-in {
     1767    0% {
     1768        opacity: 0;
     1769    }
     1770    50% {
     1771        opacity: 0;
     1772    }
     1773    100% {
     1774        opacity: 1;
     1775    }
     1776}
     1777
     1778.control-panel-themes .customize-themes-full-container.animate {
     1779    -webkit-animation: .6s themes-fade-in 1;
     1780    animation: .6s themes-fade-in 1;
     1781}
     1782
     1783.in-themes-panel:not(.animating) .control-panel-themes .filter-themes-count {
     1784    -webkit-animation: .6s themes-fade-in 1;
     1785    animation: .6s themes-fade-in 1;
     1786}
     1787
     1788.control-panel-themes .filter-themes-count {
     1789    position: relative;
     1790    float: right;
     1791    line-height: 34px;
     1792}
     1793
     1794.control-panel-themes .filter-themes-count .themes-displayed {
     1795    font-weight: 600;
     1796    color: #555d66;
     1797}
     1798
     1799.customize-themes-notifications {
     1800    margin: 0;
     1801}
     1802
     1803.control-panel-themes .customize-themes-notifications .notice {
     1804    margin: 0 0 25px 0;
     1805}
     1806
     1807.customize-themes-full-container .customize-themes-section {
     1808    display: none !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1809    overflow: hidden;
     1810}
     1811
     1812.customize-themes-full-container .customize-themes-section.current-section {
     1813    display: list-item !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1814}
     1815
     1816.control-section .customize-section-text-before {
     1817    padding: 0 0 8px 15px;
     1818    margin: 15px 0 0 0;
     1819    line-height: 16px;
     1820    border-bottom: 1px solid #ddd;
     1821    color: #555d66;
     1822}
     1823
     1824.control-panel-themes .customize-themes-section-title {
     1825    width: 100%;
     1826    background: #fff;
     1827    box-shadow: none;
     1828    outline: none;
     1829    border-top: none;
     1830    border-bottom: 1px solid #ddd;
     1831    border-left: 4px solid #fff;
     1832    border-right: none;
     1833    cursor: pointer;
     1834    padding: 10px 15px;
     1835    position: relative;
     1836    text-align: left;
    16601837    font-size: 14px;
    16611838    font-weight: 600;
    1662 }
    1663 
    1664 #customize-controls .customize-themes-panel > h2 {
    1665     padding: 15px 8px 0 8px;
    1666 }
    1667 
    1668 #customize-theme-controls .customize-themes-panel .accordion-section-content {
    1669     background: transparent;
     1839    color: #555d66;
     1840    text-shadow: none;
     1841}
     1842
     1843.control-panel-themes #accordion-section-installed_themes {
     1844    border-top: 1px solid #ddd;
     1845}
     1846
     1847.control-panel-themes .theme-section {
     1848    margin: 0;
     1849    position: relative;
     1850}
     1851
     1852.control-panel-themes .customize-themes-section-title:focus,
     1853.control-panel-themes .customize-themes-section-title:hover {
     1854    border-left-color: #0073aa;
     1855    color: #0073aa;
     1856    background: #f5f5f5;
     1857}
     1858
     1859.customize-themes-section-title:not(.selected):after {
     1860    content: "";
    16701861    display: block;
    1671 }
    1672 
    1673 .customize-control.customize-control-theme {
    1674     margin-bottom: 8px;
     1862    position: absolute;
     1863    top: 9px;
     1864    right: 15px;
     1865    width: 18px;
     1866    height: 18px;
     1867    border-radius: 100%;
     1868    border: 1px solid #ccc;
     1869    background: #fff;
     1870}
     1871
     1872.control-panel-themes .theme-section .customize-themes-section-title.selected:after {
     1873    content: "\f147";
     1874    font: 16px/1 dashicons;
     1875    box-sizing: border-box;
     1876    width: 20px;
     1877    height: 20px;
     1878    padding: 3px 3px 1px 1px; /* Re-align the icon to the smaller grid */
     1879    border-radius: 100%;
     1880    position: absolute;
     1881    top: 9px;
     1882    right: 15px;
     1883    background: #0073aa;
     1884    color: #fff;
     1885}
     1886
     1887.control-panel-themes .customize-themes-section-title.selected {
     1888    color: #0073aa;
    16751889}
    16761890
     
    16821896}
    16831897
     1898.loading .customize-themes-section .spinner {
     1899    display: block;
     1900    visibility: visible;
     1901    position: relative;
     1902    clear: both;
     1903    width: 20px;
     1904    height: 20px;
     1905    left: calc(50% - 10px);
     1906    float: none;
     1907    margin-top: 50px;
     1908}
     1909
     1910.customize-themes-section .no-themes,
     1911.customize-themes-section .no-themes-local {
     1912    display: none;
     1913}
     1914
     1915.themes-section-installed_themes .theme .notice-success {
     1916    display: none; /* Hide "installed" notice on installed themes tab. */
     1917}
     1918
     1919.control-panel-themes .theme-browser .theme .theme-actions .button-primary {
     1920    margin: 0 0 0 8px;
     1921}
     1922
     1923.customize-control-theme .theme {
     1924    width: 100%;
     1925    margin: 0;
     1926    border: 1px solid #ddd;
     1927    background: #fff;
     1928}
     1929
     1930.customize-control-theme .theme .theme-name, .customize-control-theme .theme .theme-actions {
     1931    background: #fff;
     1932    border: none;
     1933}
     1934
     1935.customize-control.customize-control-theme { /* override most properties on .customize-control */
     1936    box-sizing: border-box;
     1937    width: 25%;
     1938    max-width: 600px; /* Max. screenshot size / 2 */
     1939    margin: 0 25px 25px 0;
     1940    padding: 0;
     1941    clear: none;
     1942}
     1943
     1944/* 5 columns above 2100px */
     1945@media screen and (min-width: 2101px) {
     1946    .customize-control.customize-control-theme {
     1947        width: calc( ( 100% - 125px ) / 5 - 1px ); /* 1px offset accounts for browser rounding, typical all grids */
     1948    }
     1949}
     1950
     1951/* 4 columns up to 2100px */
     1952@media screen and (min-width: 1601px) and (max-width: 2100px) {
     1953    .customize-control.customize-control-theme {
     1954        width: calc( ( 100% - 100px ) / 4 - 1px );
     1955    }
     1956}
     1957
     1958/* 3 columns up to 1600px */
     1959@media screen and (min-width: 1201px) and (max-width: 1600px) {
     1960    .customize-control.customize-control-theme {
     1961        width: calc( ( 100% - 75px ) / 3 - 1px );
     1962    }
     1963}
     1964
     1965/* 2 columns up to 1200px */
     1966@media screen and (min-width: 851px) and (max-width: 1200px) {
     1967    .customize-control.customize-control-theme {
     1968        width: calc( ( 100% - 50px ) / 2 - 1px );
     1969
     1970    }
     1971}
     1972
     1973/* 1 column up to 850 px */
     1974@media screen and (max-width: 850px) {
     1975    .customize-control.customize-control-theme {
     1976        width: 100%;
     1977    }
     1978}
     1979
    16841980.wp-customizer .theme-browser .themes {
    1685     padding-bottom: 8px;
    1686 }
    1687 
    1688 .wp-customizer .theme-browser .theme {
    1689     margin: 0;
    1690     width: 100%;
     1981    padding: 0 0 25px 25px;
     1982    transition: .18s margin-top linear;
    16911983}
    16921984
    16931985.wp-customizer .theme-browser .theme .theme-actions {
    1694     -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
    16951986    opacity: 1;
    16961987}
     
    17041995}
    17051996
    1706 .wp-customizer #themes-filter {
    1707     font-size: 16px;
    1708     font-weight: 300;
    1709     line-height: 1.5;
    1710     width: 100%;
    1711 }
    1712 
    1713 .control-section-themes .accordion-section-title:after,
    1714 .customize-themes-panel .accordion-section-title:after {
     1997.customize-preview-header.themes-filter-bar {
     1998    position: fixed;
     1999    top: 0;
     2000    left: 300px;
     2001    width: calc(100% - 300px);
     2002    height: 46px;
     2003    background: #eee;
     2004    z-index: 10;
     2005    padding: 6px 25px;
     2006    box-sizing: border-box;
     2007    border-bottom: 1px solid #ddd;
     2008}
     2009
     2010.themes-filter-bar .themes-filter-container {
     2011    margin: 0;
     2012    padding: 0;
     2013}
     2014
     2015.themes-filter-bar .wp-filter-search {
     2016    line-height: 25px;
     2017    padding: 3px 5px;
     2018    max-width: 100%;
     2019    width: 40%;
     2020    min-width: 300px;
     2021    position: absolute;
     2022    top: 6px;
     2023    left: 25px;
     2024}
     2025
     2026/* Unstick the filter bar on short windows/screens. This breakpoint is based on the
     2027   current length of .org feature filters assuming translations do not wrap lines. */
     2028@media screen and (max-height:540px), screen and (max-width:1018px) {
     2029    .customize-preview-header.themes-filter-bar {
     2030        position: relative;
     2031        left: 0;
     2032        width: 100%;
     2033        margin: 0 0 25px 0;
     2034    }
     2035    .wp-customizer .theme-browser .themes {
     2036        padding: 0 0 25px 25px;
     2037        overflow: hidden;
     2038    }
     2039
     2040    .control-panel-themes .customize-themes-full-container {
     2041        margin-top: 0;
     2042        padding: 0;
     2043        height: 100%;
     2044        width: calc(100% - 300px);
     2045    }
     2046}
     2047
     2048@media screen and (max-width:1018px) {
     2049    .themes-filter-bar .filter-group {
     2050        width: calc( (100% - 50px) / 2);
     2051    }
     2052}
     2053
     2054@media screen and (max-width:900px) {
     2055    .customize-preview-header.themes-filter-bar {
     2056        height: 86px;
     2057        padding-top: 46px;
     2058    }
     2059
     2060    .themes-filter-bar .wp-filter-search {
     2061        width: calc(100% - 50px);
     2062        margin: 0;
     2063        min-width: 200px;
     2064    }
     2065
     2066    .themes-filter-bar .filter-drawer {
     2067        top: 86px;
     2068    }
     2069
     2070    .control-panel-themes .filter-themes-count {
     2071        float: left;
     2072    }
     2073}
     2074
     2075@media screen and (max-width:792px) {
     2076    .themes-filter-bar .filter-group {
     2077        width: calc( 100% - 25px);
     2078    }
     2079}
     2080
     2081.control-panel-themes .customize-themes-mobile-back {
    17152082    display: none;
    17162083}
    17172084
    1718 .customize-themes-panel.control-panel-content {
    1719     border-top: 1px solid #ddd;
     2085/* Mobile - toggle between themes and filters */
     2086@media screen and (max-width:600px) {
     2087
     2088    .wp-full-overlay.showing-themes .control-panel-themes .filter-themes-count .filter-themes {
     2089        display: block;
     2090        float: right;
     2091    }
     2092
     2093    .control-panel-themes .customize-themes-full-container {
     2094        width: 100%;
     2095        margin: 0;
     2096        top: 46px;
     2097        height: calc(100% - 46px);
     2098        z-index: 1;
     2099        display: none;
     2100    }
     2101
     2102    .showing-themes .control-panel-themes .customize-themes-full-container {
     2103        display: block;
     2104    }
     2105
     2106    .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back {
     2107        display: block;
     2108        position: fixed;
     2109        top: 0;
     2110        left: 0;
     2111        background: #eee;
     2112        color: #444;
     2113        border-radius: 0;
     2114        box-shadow: none;
     2115        border: none;
     2116        height: 46px;
     2117        width: 100%;
     2118        z-index: 10;
     2119        text-align: left;
     2120        text-shadow: none;
     2121        border-bottom: 1px solid #ddd;
     2122        border-left: 4px solid transparent;
     2123        margin: 0;
     2124        padding: 0;
     2125        font-size: 0;
     2126        overflow: hidden;
     2127    }
     2128
     2129    .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:before {
     2130        left: 0;
     2131        top: 0;
     2132        height: 42px;
     2133        width: 26px;
     2134        display: block;
     2135        line-height: 46px;
     2136        padding: 0 8px 0 8px;
     2137        border-right: 1px solid #ddd;
     2138    }
     2139
     2140    .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:hover,
     2141    .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:focus {
     2142        color: #0073aa;
     2143        background: #f3f3f5;
     2144        border-left-color: #0073aa;
     2145        outline: none;
     2146        box-shadow: none;
     2147    }
     2148
     2149    .showing-themes #customize-header-actions {
     2150        display: none;
     2151    }
    17202152}
    17212153
     
    17342166}
    17352167
     2168/* Avoid a z-index war by resetting elements that should be under the overlay.
     2169   This is likely required because of the way that sections and panels are positioned. */
     2170.wp-customizer.modal-open #customize-header-actions,
     2171.wp-customizer.modal-open .control-panel-themes .filter-themes-count,
     2172.wp-customizer.modal-open .control-panel-themes .customize-themes-section-title.selected:after {
     2173    z-index: -1;
     2174}
     2175
    17362176.wp-customizer .theme-overlay .theme-backdrop {
    17372177    background: rgba( 238, 238, 238, 0.75 );
    17382178    position: fixed;
    17392179    z-index: 110;
     2180}
     2181
     2182.wp-customizer .theme-overlay .star-rating {
     2183    float: left;
     2184    margin-right: 8px;
     2185}
     2186
     2187.wp-customizer .theme-rating .num-ratings {
     2188    line-height: 20px;
    17402189}
    17412190
     
    17462195    bottom: 45px;
    17472196    z-index: 120;
    1748     max-width: 1740px; /* To ensure that theme screenshots are not displayed larger than 880px wide. */
    17492197}
    17502198
    17512199.wp-customizer .theme-overlay .theme-actions {
    1752     text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */
    1753 }
    1754 
    1755 .ie8 .wp-customizer .theme-overlay .theme-header,
    1756 .ie8 .wp-customizer .theme-overlay .theme-about,
    1757 .ie8 .wp-customizer .theme-overlay .theme-actions {
    1758     position: static;
     2200    text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */
     2201    padding: 10px 25px;
     2202    background: #eee;
     2203    border-top: 1px solid #ddd;
     2204}
     2205
     2206.wp-customizer .theme-overlay .theme-actions .theme-install.preview {
     2207    margin-left: 8px;
     2208}
     2209
     2210.control-panel-themes .theme-actions .delete-theme {
     2211    left: 15px; /* these override themes.css on mobile */
     2212    right: auto;
     2213    bottom: auto;
     2214    position: absolute;
     2215}
     2216
     2217.modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content {
     2218    overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */
     2219}
     2220
     2221.wp-customizer .theme-header {
     2222    background: #eee;
     2223}
     2224
     2225.wp-customizer .theme-overlay .theme-header button,
     2226.wp-customizer .theme-overlay .theme-header .close:before {
     2227    color: #444;
     2228}
     2229
     2230.wp-customizer .theme-overlay .theme-header .close:focus,
     2231.wp-customizer .theme-overlay .theme-header .close:hover,
     2232.wp-customizer .theme-overlay .theme-header .right:focus,
     2233.wp-customizer .theme-overlay .theme-header .right:hover,
     2234.wp-customizer .theme-overlay .theme-header .left:focus,
     2235.wp-customizer .theme-overlay .theme-header .left:hover {
     2236    background: #fff;
     2237    border-bottom: 4px solid #0073aa;
     2238    color: #0073aa;
     2239}
     2240
     2241.wp-customizer .theme-overlay .theme-header .close:focus:before,
     2242.wp-customizer .theme-overlay .theme-header .close:hover:before {
     2243    color: #0073aa;
     2244}
     2245
     2246.wp-customizer .theme-overlay .theme-header button.disabled,
     2247.wp-customizer .theme-overlay .theme-header button.disabled:hover,
     2248.wp-customizer .theme-overlay .theme-header button.disabled:focus {
     2249    border-bottom: none;
     2250    background: transparent;
     2251    color: #ccc;
    17592252}
    17602253
     
    17842277    border-bottom: 1px solid #ddd;
    17852278    clear: both;
    1786     color: #666;
     2279    color: #555d66;
    17872280    font-size: 24px;
    17882281    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
  • trunk/src/wp-admin/css/install.css

    r41622 r41648  
    405405    border: 0;
    406406    clip: rect(1px, 1px, 1px, 1px);
     407    -webkit-clip-path: inset(50%);
    407408    clip-path: inset(50%);
    408409    height: 1px;
  • trunk/src/wp-admin/css/list-tables.css

    r41622 r41648  
    18221822    .post-com-count .screen-reader-text {
    18231823        position: static;
     1824        -webkit-clip-path: none;
    18241825        clip-path: none;
    18251826        width: auto;
  • trunk/src/wp-admin/css/nav-menus.css

    r41622 r41648  
    600600.no-js.nav-menus-php .item-edit .screen-reader-text {
    601601    position: static;
     602    -webkit-clip-path: none;
    602603    clip-path: none;
    603604    width: auto;
  • trunk/src/wp-admin/css/themes.css

    r41062 r41648  
    550550    margin: 0 30px 0 0;
    551551    width: 55%;
    552     max-width: 880px;
     552    max-width: 1200px; /* Recommended theme screenshot width, set here to avoid stretching */
    553553    text-align: center;
    554554}
     
    10501050}
    10511051
    1052 p.no-themes {
     1052p.no-themes,
     1053p.no-themes-local {
    10531054    clear: both;
    10541055    color: #666;
     
    17061707}
    17071708
    1708 #customize-container {
     1709#customize-container,
     1710#customize-themes-loading-container {
    17091711    display: none;
    1710     background: #fff;
     1712    background: #eee;
    17111713    z-index: 500000;
    17121714    position: fixed;
     
    17211723/* Make the Customizer and Theme installer overlays the only available content. */
    17221724#customize-container,
     1725#customize-themes-loading-container,
    17231726.theme-install-overlay {
    17241727    visibility: visible;
     
    18251828#customize-preview.wp-full-overlay-main:before,
    18261829.customize-loading #customize-container:before,
     1830.customize-loading #customize-themes-loading-container:before,
    18271831.theme-install-overlay .wp-full-overlay-main:before {
    18281832    content: "";
     
    18621866    #customize-preview.wp-full-overlay-main:before,
    18631867    .customize-loading #customize-container:before,
     1868    .customize-loading #customize-themes-loading-container:before,
    18641869    .theme-install-overlay .wp-full-overlay-main:before {
    18651870        background-image: url(../images/spinner-2x.gif);
  • trunk/src/wp-admin/customize.php

    r41626 r41648  
    126126
    127127<script type="text/javascript">
    128 var ajaxurl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php', 'relative' ) ); ?>;
     128var ajaxurl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php', 'relative' ) ); ?>,
     129    pagenow = 'customize';
    129130</script>
    130131
  • trunk/src/wp-admin/includes/theme.php

    r41605 r41648  
    235235    $features = array(
    236236
    237         __( 'Layout' ) => array(
    238             'grid-layout'   => __( 'Grid Layout' ),
    239             'one-column'    => __( 'One Column' ),
    240             'two-columns'   => __( 'Two Columns' ),
    241             'three-columns' => __( 'Three Columns' ),
    242             'four-columns'  => __( 'Four Columns' ),
    243             'left-sidebar'  => __( 'Left Sidebar' ),
    244             'right-sidebar' => __( 'Right Sidebar' ),
    245         ),
    246 
    247         __( 'Features' ) => array(
    248             'accessibility-ready'   => __( 'Accessibility Ready' ),
    249             'buddypress'            => __( 'BuddyPress' ),
    250             'custom-background'     => __( 'Custom Background' ),
    251             'custom-colors'         => __( 'Custom Colors' ),
    252             'custom-header'         => __( 'Custom Header' ),
    253             'custom-logo'           => __( 'Custom Logo' ),
    254             'custom-menu'           => __( 'Custom Menu' ),
    255             'editor-style'          => __( 'Editor Style' ),
    256             'featured-image-header' => __( 'Featured Image Header' ),
    257             'featured-images'       => __( 'Featured Images' ),
    258             'flexible-header'       => __( 'Flexible Header' ),
    259             'footer-widgets'        => __( 'Footer Widgets' ),
    260             'front-page-post-form'  => __( 'Front Page Posting' ),
    261             'full-width-template'   => __( 'Full Width Template' ),
    262             'microformats'          => __( 'Microformats' ),
    263             'post-formats'          => __( 'Post Formats' ),
    264             'rtl-language-support'  => __( 'RTL Language Support' ),
    265             'sticky-post'           => __( 'Sticky Post' ),
    266             'theme-options'         => __( 'Theme Options' ),
    267             'threaded-comments'     => __( 'Threaded Comments' ),
    268             'translation-ready'     => __( 'Translation Ready' ),
    269         ),
    270 
    271237        __( 'Subject' )  => array(
    272238            'blog'           => __( 'Blog' ),
     
    279245            'photography'    => __( 'Photography' ),
    280246            'portfolio'      => __( 'Portfolio' ),
     247        ),
     248
     249        __( 'Features' ) => array(
     250            'accessibility-ready'   => __( 'Accessibility Ready' ),
     251            'custom-background'     => __( 'Custom Background' ),
     252            'custom-colors'         => __( 'Custom Colors' ),
     253            'custom-header'         => __( 'Custom Header' ),
     254            'custom-logo'           => __( 'Custom Logo' ),
     255            'editor-style'          => __( 'Editor Style' ),
     256            'featured-image-header' => __( 'Featured Image Header' ),
     257            'featured-images'       => __( 'Featured Images' ),
     258            'footer-widgets'        => __( 'Footer Widgets' ),
     259            'full-width-template'   => __( 'Full Width Template' ),
     260            'post-formats'          => __( 'Post Formats' ),
     261            'sticky-post'           => __( 'Sticky Post' ),
     262            'theme-options'         => __( 'Theme Options' ),
     263        ),
     264
     265        __( 'Layout' ) => array(
     266            'grid-layout'   => __( 'Grid Layout' ),
     267            'one-column'    => __( 'One Column' ),
     268            'two-columns'   => __( 'Two Columns' ),
     269            'three-columns' => __( 'Three Columns' ),
     270            'four-columns'  => __( 'Four Columns' ),
     271            'left-sidebar'  => __( 'Left Sidebar' ),
     272            'right-sidebar' => __( 'Right Sidebar' ),
    281273        )
     274
    282275    );
    283276
     
    575568        $parent = false;
    576569        if ( $theme->parent() ) {
    577             $parent = $theme->parent()->display( 'Name' );
    578             $parents[ $slug ] = $theme->parent()->get_stylesheet();
     570            $parent = $theme->parent();
     571            $parents[ $slug ] = $parent->get_stylesheet();
     572            $parent = $parent->display( 'Name' );
    579573        }
    580574
     
    636630 */
    637631function customize_themes_print_templates() {
    638     $preview_url = esc_url( add_query_arg( 'theme', '__THEME__' ) ); // Token because esc_url() strips curly braces.
    639     $preview_url = str_replace( '__THEME__', '{{ data.id }}', $preview_url );
    640632    ?>
    641633    <script type="text/html" id="tmpl-customize-themes-details-view">
     
    649641            <div class="theme-about wp-clearfix">
    650642                <div class="theme-screenshots">
    651                 <# if ( data.screenshot[0] ) { #>
     643                <# if ( data.screenshot && data.screenshot[0] ) { #>
    652644                    <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div>
    653645                <# } else { #>
     
    662654                    <h2 class="theme-name">{{{ data.name }}}<span class="theme-version"><?php printf( __( 'Version: %s' ), '{{ data.version }}' ); ?></span></h2>
    663655                    <h3 class="theme-author"><?php printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' ); ?></h3>
    664                     <p class="theme-description">{{{ data.description }}}</p>
     656
     657                    <# if ( data.stars && 0 != data.num_ratings ) { #>
     658                        <div class="theme-rating">
     659                            {{{ data.stars }}}
     660                            <span class="num-ratings">
     661                                <?php
     662                                /* translators: %s is the number of ratings */
     663                                echo sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' );
     664                                ?>
     665                            </span>
     666                        </div>
     667                    <# } #>
     668
     669                    <# if ( data.hasUpdate ) { #>
     670                        <div class="notice notice-warning notice-alt notice-large" data-slug="{{ data.id }}">
     671                            <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
     672                            {{{ data.update }}}
     673                        </div>
     674                    <# } #>
    665675
    666676                    <# if ( data.parent ) { #>
     
    668678                    <# } #>
    669679
     680                    <p class="theme-description">{{{ data.description }}}</p>
     681
    670682                    <# if ( data.tags ) { #>
    671                         <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{ data.tags }}</p>
     683                        <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
    672684                    <# } #>
    673685                </div>
    674686            </div>
    675687
    676             <# if ( ! data.active ) { #>
    677                 <div class="theme-actions">
    678                     <div class="inactive-theme">
    679                         <?php
    680                         /* translators: %s: Theme name */
    681                         $aria_label = sprintf( __( 'Preview %s' ), '{{ data.name }}' );
    682                         ?>
    683                         <a href="<?php echo $preview_url; ?>" target="_top" class="button button-primary" aria-label="<?php echo esc_attr( $aria_label ); ?>"><?php _e( 'Live Preview' ); ?></a>
    684                     </div>
    685                 </div>
    686             <# } #>
     688            <div class="theme-actions">
     689                <# if ( data.active ) { #>
     690                    <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></a>
     691                <# } else if ( 'installed' === data.type ) { #>
     692                    <?php if ( current_user_can( 'delete_themes' ) ) { ?>
     693                        <# if ( data.actions && data.actions['delete'] ) { #>
     694                            <a href="{{{ data.actions['delete'] }}}" data-slug="{{ data.id }}" class="button button-secondary delete-theme"><?php _e( 'Delete' ); ?></a>
     695                        <# } #>
     696                    <?php } ?>
     697                    <button type="button" class="button button-primary preview-theme" data-slug="{{ data.id }}"><?php _e( 'Live Preview' ); ?></span>
     698                <# } else { #>
     699                    <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
     700                    <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}"><?php _e( 'Install &amp; Preview' ); ?></button>
     701                <# } #>
     702            </div>
    687703        </div>
    688704    </script>
  • 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();
  • trunk/src/wp-admin/js/updates.js

    r41609 r41648  
    184184            $notice.replaceWith( $adminNotice );
    185185        } else {
    186             $( '.wrap' ).find( '> h1' ).after( $adminNotice );
     186            if ( 'customize' === pagenow ) {
     187                $( '.customize-themes-notifications' ).append( $adminNotice );
     188            } else {
     189                $( '.wrap' ).find( '> h1' ).after( $adminNotice );
     190            }
    187191        }
    188192
     
    931935            $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
    932936
     937        } else if ( 'customize' === pagenow ) {
     938
     939            // Update the theme details UI.
     940            $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
     941
     942            $notice.find( 'h3' ).remove();
     943
     944            // Add the top-level UI, and update both.
     945            $notice = $notice.add( $( '#customize-control-theme-installed_' + args.slug ).find( '.update-message' ) );
     946            $notice = $notice.addClass( 'updating-message' ).find( 'p' );
     947
    933948        } else {
    934949            $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
     
    973988            $notice, newText;
    974989
     990        if ( 'customize' === pagenow ) {
     991            $theme = wp.customize.control( 'installed_theme_' + response.slug ).container;
     992        }
     993
    975994        if ( 'themes-network' === pagenow ) {
    976995            $notice = $theme.find( '.update-message' );
     
    10251044        if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
    10261045            return;
     1046        }
     1047
     1048        if ( 'customize' === pagenow ) {
     1049            $theme = wp.customize.control( 'installed_theme_' + response.slug ).container;
    10271050        }
    10281051
     
    11631186        }
    11641187
    1165         if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
    1166             $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
    1167             $card   = $( '.install-theme-info' ).prepend( $message );
     1188        if ( 'customize' === pagenow ) {
     1189            if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) {
     1190                $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
     1191                $card   = $( '.theme-overlay .theme-info' ).prepend( $message );
     1192            } else {
     1193                $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
     1194                $card   = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message );
     1195            }
     1196            $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
    11681197        } else {
    1169             $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
    1170             $button = $card.find( '.theme-install' );
     1198            if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
     1199                $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
     1200                $card   = $( '.install-theme-info' ).prepend( $message );
     1201            } else {
     1202                $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
     1203                $button = $card.find( '.theme-install' );
     1204            }
    11711205        }
    11721206
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r41640 r41648  
    321321        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
    322322
     323        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
    323324        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
    324325        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
     
    376377        add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
    377378        add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
     379        add_action( 'wp_ajax_customize-load-themes',    array( $this, 'load_themes_ajax' ) );
    378380        add_action( 'wp_ajax_dismiss_customize_changeset_autosave', array( $this, 'handle_dismiss_changeset_autosave_request' ) );
    379381
     
    393395        // Export the settings to JS via the _wpCustomizeSettings variable.
    394396        add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
     397
     398        // Add theme update notices.
     399        if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
     400            require_once ABSPATH . '/wp-admin/includes/update.php';
     401            add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
     402        }
    395403    }
    396404
     
    36863694            $control->enqueue();
    36873695        }
     3696
     3697        if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
     3698            wp_enqueue_script( 'updates' );
     3699        }
    36883700    }
    36893701
     
    38903902            'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
    38913903            'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
     3904            'switch-themes' => wp_create_nonce( 'switch-themes' ),
    38923905            'dismiss_autosave' => wp_create_nonce( 'dismiss_customize_changeset_autosave' ),
    38933906        );
     
    39964009            'documentTitleTmpl' => $this->get_document_title_template(),
    39974010            'previewableDevices' => $this->get_previewable_devices(),
     4011            'l10n' => array(
     4012                'confirmDeleteTheme' => __( 'Are you sure you want to delete this theme?' ),
     4013                /* translators: %d is the number of theme search results, which cannot currently consider singular vs. plural forms */
     4014                'themeSearchResults' => __( '%d themes found' ),
     4015                /* translators: %d is the number of themes being displayed, which cannot currently consider singular vs. plural forms */
     4016                'announceThemeCount' => __( 'Displaying %d themes' ),
     4017                /* translators: %s is the theme name */
     4018                'announceThemeDetails' => __( 'Showing details for theme: %s' ),
     4019            ),
    39984020        );
    39994021
     
    40994121        /* Panel, Section, and Control Types */
    41004122        $this->register_panel_type( 'WP_Customize_Panel' );
     4123        $this->register_panel_type( 'WP_Customize_Themes_Panel' );
    41014124        $this->register_section_type( 'WP_Customize_Section' );
    41024125        $this->register_section_type( 'WP_Customize_Sidebar_Section' );
     4126        $this->register_section_type( 'WP_Customize_Themes_Section' );
    41034127        $this->register_control_type( 'WP_Customize_Color_Control' );
    41044128        $this->register_control_type( 'WP_Customize_Media_Control' );
     
    41604184        ) ) );
    41614185
    4162         /* Themes */
    4163 
    4164         $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
    4165             'title'      => $this->theme()->display( 'Name' ),
    4166             'capability' => 'switch_themes',
    4167             'priority'   => 0,
     4186        /* Themes (controls are loaded via ajax) */
     4187
     4188        $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array(
     4189            'title'       => $this->theme()->display( 'Name' ),
     4190            'description' => __( 'Once themes are installed, you can live-preview them on your site, customize them, and publish your new design. Browse available themes via the filters in this menu.' ),
     4191            'capability'  => 'switch_themes',
     4192            'priority'    => 0,
    41684193        ) ) );
     4194
     4195        $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array(
     4196            'title'       => __( 'Installed themes' ),
     4197            'action'      => 'installed',
     4198            'capability'  => 'switch_themes',
     4199            'panel'       => 'themes',
     4200            'priority'    => 0,
     4201        ) ) );
     4202
     4203        if ( ! is_multisite() ) {
     4204            $this->add_section( new WP_Customize_Themes_Section( $this, 'wporg_themes', array(
     4205                'title'       => __( 'WordPress.org themes' ),
     4206                'action'      => 'wporg',
     4207                'capability'  => 'install_themes',
     4208                'panel'       => 'themes',
     4209                'priority'    => 5,
     4210            ) ) );
     4211        }
    41694212
    41704213        // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
     
    41724215            'capability' => 'switch_themes',
    41734216        ) ) );
    4174 
    4175         require_once( ABSPATH . 'wp-admin/includes/theme.php' );
    4176 
    4177         // Theme Controls.
    4178 
    4179         // Add a control for the active/original theme.
    4180         if ( ! $this->is_theme_active() ) {
    4181             $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
    4182             $active_theme = current( $themes );
    4183             $active_theme['isActiveTheme'] = true;
    4184             $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
    4185                 'theme'    => $active_theme,
    4186                 'section'  => 'themes',
    4187                 'settings' => 'active_theme',
    4188             ) ) );
    4189         }
    4190 
    4191         $themes = wp_prepare_themes_for_js();
    4192         foreach ( $themes as $theme ) {
    4193             if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
    4194                 continue;
    4195             }
    4196 
    4197             $theme_id = 'theme_' . $theme['id'];
    4198             $theme['isActiveTheme'] = false;
    4199             $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
    4200                 'theme'    => $theme,
    4201                 'section'  => 'themes',
    4202                 'settings' => 'active_theme',
    4203             ) ) );
    4204         }
    42054217
    42064218        /* Site Identity */
     
    47084720
    47094721    /**
     4722     * Load themes into the theme browsing/installation UI.
     4723     *
     4724     * @since 4.9.0
     4725     */
     4726    public function load_themes_ajax() {
     4727        check_ajax_referer( 'switch-themes', 'switch-themes-nonce' );
     4728
     4729        if ( ! current_user_can( 'switch_themes' ) ) {
     4730            wp_die( -1 );
     4731        }
     4732
     4733        if ( empty( $_POST['theme_action'] ) ) {
     4734            wp_send_json_error( 'missing_theme_action' );
     4735        }
     4736        $theme_action = sanitize_key( $_POST['theme_action'] );
     4737        $themes = array();
     4738
     4739        require_once ABSPATH . 'wp-admin/includes/theme.php';
     4740        if ( 'installed' === $theme_action ) {
     4741            $themes = array( 'themes' => wp_prepare_themes_for_js() );
     4742            foreach ( $themes['themes'] as &$theme ) {
     4743                $theme['type'] = 'installed';
     4744                $theme['active'] = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme['id'] );
     4745            }
     4746        } elseif ( 'wporg' === $theme_action ) {
     4747            if ( ! current_user_can( 'install_themes' ) ) {
     4748                wp_die( -1 );
     4749            }
     4750
     4751            // Arguments for all queries.
     4752            $args = array(
     4753                'per_page' => 100,
     4754                'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
     4755                'fields' => array(
     4756                    'screenshot_url' => true,
     4757                    'description' => true,
     4758                    'rating' => true,
     4759                    'downloaded' => true,
     4760                    'downloadlink' => true,
     4761                    'last_updated' => true,
     4762                    'homepage' => true,
     4763                    'num_ratings' => true,
     4764                    'tags' => true,
     4765                    'parent' => true,
     4766                    // 'extended_author' => true, @todo: WordPress.org throws a 500 server error when this is here.
     4767                ),
     4768            );
     4769
     4770            // Define query filters based on user input.
     4771            if ( ! array_key_exists( 'search', $_POST ) ) {
     4772                $args['search'] = '';
     4773            } else {
     4774                $args['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) );
     4775            }
     4776
     4777            if ( ! array_key_exists( 'tags', $_POST ) ) {
     4778                $args['tag'] = '';
     4779            } else {
     4780                $args['tag'] = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['tags'] ) );
     4781            }
     4782
     4783            if ( '' === $args['search'] && '' === $args['tag'] ) {
     4784                $args['browse'] = 'new'; // Sort by latest themes by default.
     4785            }
     4786
     4787            // Load themes from the .org API.
     4788            $themes = themes_api( 'query_themes', $args );
     4789            if ( is_wp_error( $themes ) ) {
     4790                wp_send_json_error();
     4791            }
     4792
     4793            // This list matches the allowed tags in wp-admin/includes/theme-install.php.
     4794            $themes_allowedtags = array_fill_keys(
     4795                array( 'a', 'abbr', 'acronym', 'code', 'pre', 'em', 'strong', 'div', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' ),
     4796                array()
     4797            );
     4798            $themes_allowedtags['a'] = array_fill_keys( array( 'href', 'title', 'target' ), true );
     4799            $themes_allowedtags['acronym']['title'] = true;
     4800            $themes_allowedtags['abbr']['title'] = true;
     4801            $themes_allowedtags['img'] = array_fill_keys( array( 'src', 'class', 'alt' ), true );
     4802
     4803            // Prepare a list of installed themes to check against before the loop.
     4804            $installed_themes = array();
     4805            $wp_themes = wp_get_themes();
     4806            foreach ( $wp_themes as $theme ) {
     4807                $installed_themes[] = $theme->get_stylesheet();
     4808            }
     4809            $update_php = network_admin_url( 'update.php?action=install-theme' );
     4810
     4811            // Set up properties for themes available on WordPress.org.
     4812            foreach ( $themes->themes as &$theme ) {
     4813                $theme->install_url = add_query_arg( array(
     4814                    'theme'    => $theme->slug,
     4815                    '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
     4816                ), $update_php );
     4817
     4818                $theme->name        = wp_kses( $theme->name, $themes_allowedtags );
     4819                $theme->author      = wp_kses( $theme->author, $themes_allowedtags );
     4820                $theme->version     = wp_kses( $theme->version, $themes_allowedtags );
     4821                $theme->description = wp_kses( $theme->description, $themes_allowedtags );
     4822                $theme->tags        = implode( ', ', $theme->tags );
     4823                $theme->stars       = wp_star_rating( array(
     4824                    'rating' => $theme->rating,
     4825                    'type' => 'percent',
     4826                    'number' => $theme->num_ratings,
     4827                    'echo' => false,
     4828                ) );
     4829                $theme->num_ratings = number_format_i18n( $theme->num_ratings );
     4830                $theme->preview_url = set_url_scheme( $theme->preview_url );
     4831
     4832                // Handle themes that are already installed as installed themes.
     4833                if ( in_array( $theme->slug, $installed_themes, true ) ) {
     4834                    $theme->type = 'installed';
     4835                } else {
     4836                    $theme->type = $theme_action;
     4837                }
     4838
     4839                // Set active based on customized theme.
     4840                $theme->active = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme->slug );
     4841
     4842                // Map available theme properties to installed theme properties.
     4843                $theme->id           = $theme->slug;
     4844                $theme->screenshot   = array( $theme->screenshot_url );
     4845                $theme->authorAndUri = $theme->author;
     4846                $theme->parent       = ( $theme->slug === $theme->template ) ? false : $theme->template; // The .org API does not seem to return the parent in a documented way; however, this check should yield a similar result in most cases.
     4847                unset( $theme->slug );
     4848                unset( $theme->screenshot_url );
     4849                unset( $theme->author );
     4850            } // End foreach().
     4851        } // End if().
     4852        wp_send_json_success( $themes );
     4853    }
     4854
     4855
     4856    /**
    47104857     * Callback for validating the header_textcolor value.
    47114858     *
  • trunk/src/wp-includes/css/admin-bar.css

    r41622 r41648  
    694694    border: 0;
    695695    clip: rect(1px, 1px, 1px, 1px);
     696    -webkit-clip-path: inset(50%);
    696697    clip-path: inset(50%);
    697698    height: 1px;
  • trunk/src/wp-includes/css/wp-embed-template.css

    r41622 r41648  
    1212    border: 0;
    1313    clip: rect(1px, 1px, 1px, 1px);
     14    -webkit-clip-path: inset(50%);
    1415    clip-path: inset(50%);
    1516    height: 1px;
  • trunk/src/wp-includes/customize/class-wp-customize-theme-control.php

    r41162 r41648  
    5858     */
    5959    public function content_template() {
    60         $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
    61         $active_url  = esc_url( remove_query_arg( 'customize_theme', $current_url ) );
    62         $preview_url = esc_url( add_query_arg( 'customize_theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
    63         $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url );
     60        /* translators: %s: theme name */
     61        $details_label = sprintf( __( 'Details for theme: %s' ), '{{ data.theme.name }}' );
     62        /* translators: %s: theme name */
     63        $customize_label = sprintf( __( 'Customize theme: %s' ), '{{ data.theme.name }}' );
     64        /* translators: %s: theme name */
     65        $preview_label = sprintf( __( 'Live preview theme: %s' ), '{{ data.theme.name }}' );
     66        /* translators: %s: theme name */
     67        $install_label = sprintf( __( 'Install and preview theme: %s' ), '{{ data.theme.name }}' );
    6468        ?>
    65         <# if ( data.theme.isActiveTheme ) { #>
    66             <div class="theme active" tabindex="0" data-preview-url="<?php echo esc_attr( $active_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
     69        <# if ( data.theme.active ) { #>
     70            <div class="theme active" tabindex="0" aria-describedby="{{ data.section }}-{{ data.theme.id }}-action {{ data.theme.id }}-name">
    6771        <# } else { #>
    68             <div class="theme" tabindex="0" data-preview-url="<?php echo esc_attr( $preview_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
     72            <div class="theme" tabindex="0" aria-describedby="{{ data.section }}-{{ data.theme.id }}-action {{ data.theme.id }}-name">
    6973        <# } #>
    7074
    71             <# if ( data.theme.screenshot[0] ) { #>
     75            <# if ( data.theme.screenshot && data.theme.screenshot[0] ) { #>
    7276                <div class="theme-screenshot">
    7377                    <img data-src="{{ data.theme.screenshot[0] }}" alt="" />
     
    7781            <# } #>
    7882
    79             <# if ( data.theme.isActiveTheme ) { #>
    80                 <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Customize' ); ?></span>
    81             <# } else { #>
    82                 <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Live Preview' ); ?></span>
    83             <# } #>
     83            <span class="more-details theme-details" id="{{ data.section }}-{{ data.theme.id }}-action" aria-label="<?php echo esc_attr( $details_label ); ?>"><?php _e( 'Theme Details' ); ?></span>
    8484
    8585            <div class="theme-author"><?php
     
    8888            ?></div>
    8989
    90             <# if ( data.theme.isActiveTheme ) { #>
    91                 <h3 class="theme-name" id="{{ data.theme.id }}-name">
     90            <# if ( 'installed' === data.theme.type && data.theme.hasUpdate ) { #>
     91                <div class="update-message notice inline notice-warning notice-alt" data-slug="{{ data.theme.id }}">
     92                    <p>
     93                        <?php
     94                        /* translators: %s is the linked update now button */
     95                        printf( __( 'New version available. %s' ), '<button class="button-link update-theme" type="button">' . __( 'Update now' ) . '</button>' );
     96                        ?>
     97                    </p>
     98                </div>
     99            <# } #>
     100
     101            <# if ( data.theme.active ) { #>
     102                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">
    92103                    <?php
    93104                    /* translators: %s: theme name */
    94                     printf( __( '<span>Active:</span> %s' ), '{{{ data.theme.name }}}' );
     105                    printf( __( '<span>Previewing:</span> %s' ), '{{ data.theme.name }}' );
    95106                    ?>
    96107                </h3>
     108                <div class="theme-actions">
     109                    <button type="button" class="button button-primary customize-theme" aria-label="<?php echo esc_attr( $customize_label ); ?>"><?php _e( 'Customize' ); ?></button>
     110                </div>
     111                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
     112            <# } else if ( 'installed' === data.theme.type ) { #>
     113                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
     114                <div class="theme-actions">
     115                    <button type="button" class="button button-primary preview-theme" aria-label="<?php echo esc_attr( $preview_label ); ?>" data-slug="{{ data.theme.id }}"><?php _e( 'Live Preview' ); ?></span>
     116                </div>
     117                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
    97118            <# } else { #>
    98                 <h3 class="theme-name" id="{{ data.theme.id }}-name">{{{ data.theme.name }}}</h3>
     119                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
    99120                <div class="theme-actions">
    100                     <button type="button" class="button theme-details"><?php _e( 'Theme Details' ); ?></button>
     121                    <button type="button" class="button button-primary theme-install preview" aria-label="<?php echo esc_attr( $install_label ); ?>" data-slug="{{ data.theme.id }}" data-name="{{ data.theme.name }}"><?php _e( 'Install &amp; Preview' ); ?></button>
    101122                </div>
    102123            <# } #>
  • trunk/src/wp-includes/customize/class-wp-customize-themes-section.php

    r41368 r41648  
    1111 * Customize Themes Section class.
    1212 *
    13  * A UI container for theme controls, which behaves like a backwards Panel.
     13 * A UI container for theme controls, which are displayed within sections.
    1414 *
    1515 * @since 4.2.0
     
    2020
    2121    /**
    22      * Customize section type.
     22     * Section type.
    2323     *
    2424     * @since 4.2.0
     
    2828
    2929    /**
    30      * Render the themes section, which behaves like a panel.
     30     * Theme section action.
    3131     *
    32      * @since 4.2.0
     32     * Defines the type of themes to load (installed, wporg, etc.).
     33     *
     34     * @since 4.9.0
     35     * @var string
    3336     */
    34     protected function render() {
    35         $classes = 'accordion-section control-section control-section-' . $this->type;
     37    public $action = '';
     38
     39    /**
     40     * Get section parameters for JS.
     41     *
     42     * @since 4.9.0
     43     * @return array Exported parameters.
     44     */
     45    public function json() {
     46        $exported = parent::json();
     47        $exported['action'] = $this->action;
     48
     49        return $exported;
     50    }
     51
     52    /**
     53     * Render a themes section as a JS template.
     54     *
     55     * The template is only rendered by PHP once, so all actions are prepared at once on the server side.
     56     *
     57     * @since 4.9.0
     58     */
     59    protected function render_template() {
    3660        ?>
    37         <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>">
    38             <h3 class="accordion-section-title">
    39                 <?php
    40                 if ( $this->manager->is_theme_active() ) {
    41                     echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title;
    42                 } else {
    43                     echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title;
    44                 }
    45                 ?>
    46 
    47                 <?php if ( count( $this->controls ) > 0 ) : ?>
    48                     <button type="button" class="button change-theme" tabindex="0"><?php _ex( 'Change', 'theme' ); ?></button>
    49                 <?php endif; ?>
    50             </h3>
    51             <div class="customize-themes-panel control-panel-content themes-php">
    52                 <h3 class="accordion-section-title customize-section-title">
    53                     <button class="customize-section-back" tabindex="0" type="button"><span class="screen-reader-text"><?php _e( 'Back' ); ?></span></button>
    54                     <span class="customize-action"><?php _e( 'Customizing' ); ?></span>
    55                     <?php _e( 'Themes' ); ?>
    56                     <span class="title-count theme-count"><?php echo count( $this->controls ) + 1 /* Active theme */; ?></span>
    57                 </h3>
    58                 <h3 class="accordion-section-title customize-section-title">
    59                     <?php
    60                     if ( $this->manager->is_theme_active() ) {
    61                         echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title;
    62                     } else {
    63                         echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title;
    64                     }
    65                     ?>
    66                     <button type="button" class="button customize-theme"><?php _e( 'Customize' ); ?></button>
    67                 </h3>
    68 
     61        <li id="accordion-section-{{ data.id }}" class="theme-section">
     62            <button type="button" class="customize-themes-section-title themes-section-{{ data.id }}">{{ data.title }}</button>
     63            <?php if ( current_user_can( 'install_themes' ) || is_multisite() ) : // @todo: upload support ?>
     64            <?php endif; ?>
     65            <div class="customize-themes-section themes-section-{{ data.id }} control-section-content themes-php">
    6966                <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
    70 
    71                 <div id="customize-container"></div>
    72                 <?php if ( count( $this->controls ) > 4 ) : ?>
    73                     <p><label for="themes-filter">
    74                         <span class="screen-reader-text"><?php _e( 'Search installed themes&hellip;' ); ?></span>
    75                         <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search installed themes&hellip;' ); ?>" />
    76                     </label></p>
    77                 <?php endif; ?>
    7867                <div class="theme-browser rendered">
    79                     <ul class="themes accordion-section-content">
     68                    <div class="customize-preview-header themes-filter-bar">
     69                        <?php $this->filter_bar_content_template(); ?>
     70                    </div>
     71                    <div class="error unexpected-error" style="display: none; "><p><?php _e( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="https://wordpress.org/support/">support forums</a>.' ); ?></p></div>
     72                    <ul class="themes">
    8073                    </ul>
     74                    <p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p>
     75                    <p class="no-themes-local">
     76                        <?php
     77                        /* translators: %s is the string, "search WordPress.org themes" */
     78                        printf( __( 'No themes found. Try a different search, or %s.' ),
     79                            sprintf( '<button type="button" class="button-link search-dotorg-themes">%s</button>', __( 'Search WordPress.org themes' ) )
     80                        );
     81                        ?>
     82                    </p>
     83                    <p class="spinner"></p>
    8184                </div>
    8285            </div>
    8386        </li>
    84 <?php }
     87        <?php
     88    }
     89
     90    /**
     91     * Render the filter bar portion of a themes section as a JS template.
     92     *
     93     * The template is only rendered by PHP once, so all actions are prepared at once on the server side.
     94     * The filter bar container is rendered by @see `render_template()`.
     95     *
     96     * @since 4.9.0
     97     */
     98    protected function filter_bar_content_template() {
     99        ?>
     100        <button type="button" class="button button-primary customize-section-back customize-themes-mobile-back"><?php _e( 'Back to theme sources' ); ?></button>
     101        <# if ( 'wporg' === data.action ) { #>
     102            <div class="search-form">
     103                <label class="screen-reader-text" for="wp-filter-search-input"><?php _e( 'Search themes&hellip;' ); ?></label>
     104                <input placeholder="<?php _e( 'Search themes&hellip;' ); ?>" type="search" aria-describedby="live-search-desc" id="wp-filter-search-input" class="wp-filter-search">
     105                <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span>
     106            </div>
     107            <button type="button" class="button feature-filter-toggle">
     108                <span class="filter-count-0"><?php _e( 'Filter themes' ); ?></span><span class="filter-count-filters">
     109                <?php
     110                /* translators: %s: number of filters selected. */
     111                printf( __( 'Filter themes (%s)' ), '<span class="theme-filter-count">0</span>' );
     112                ?>
     113                </span>
     114            </button>
     115            <div class="filter-drawer filter-details">
     116                <?php
     117                $feature_list = get_theme_feature_list( false ); // @todo: Use the .org API instead of the local core feature list. The .org API is currently outdated and will be reconciled when the .org themes directory is next redesigned.
     118                foreach ( $feature_list as $feature_name => $features ) {
     119                    echo '<fieldset class="filter-group">';
     120                    echo '<legend>' . esc_html( $feature_name ) . '</legend>';
     121                    echo '<div class="filter-group-feature">';
     122                    foreach ( $features as $feature => $feature_name ) {
     123                        echo '<input type="checkbox" id="filter-id-' . esc_attr( $feature ) . '" value="' . esc_attr( $feature ) . '" /> ';
     124                        echo '<label for="filter-id-' . esc_attr( $feature ) . '">' . esc_html( $feature_name ) . '</label><br>';
     125                    }
     126                    echo '</div>';
     127                    echo '</fieldset>';
     128                }
     129                ?>
     130            </div>
     131        <# } else { #>
     132            <p class="themes-filter-container">
     133                <label for="themes-filter">
     134                    <span class="screen-reader-text"><?php _e( 'Search themes&hellip;' ); ?></span>
     135                    <input type="search" id="themes-filter" placeholder="<?php esc_attr_e( 'Search themes&hellip;' ); ?>" aria-describedby="live-search-desc" class="wp-filter-search wp-filter-search-themes" />
     136                    <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span>
     137                </label>
     138            </p>
     139        <# } #>
     140        <div class="filter-themes-count">
     141            <span class="themes-displayed">
     142                <?php
     143                /* translators: %s: number of themes displayed. */
     144                echo sprintf( __( '%s themes' ), '<span class="theme-count">0</span>' );
     145                ?>
     146            </span>
     147        </div>
     148        <?php
     149    }
    85150}
  • trunk/tests/phpunit/tests/customize/manager.php

    r41626 r41648  
    25542554        $this->assertNotEmpty( $data );
    25552555
    2556         $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts', 'initialClientTimestamp', 'initialServerDate', 'initialServerTimestamp' ), array_keys( $data ) );
     2556        $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts', 'initialClientTimestamp', 'initialServerDate', 'initialServerTimestamp', 'l10n' ), array_keys( $data ) );
    25572557        $this->assertEquals( $autofocus, $data['autofocus'] );
    25582558        $this->assertArrayHasKey( 'save', $data['nonce'] );
Note: See TracChangeset for help on using the changeset viewer.