Make WordPress Core

Changeset 41648


Ignore:
Timestamp:
09/29/2017 08:12:19 PM (9 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.