WordPress.org

Make WordPress Core

Ticket #37661: 37661.11.diff

File 37661.11.diff, 99.4 KB (added by celloexpressions, 2 years ago)

Implement new design by @folletto, see forthcoming comment for details.

  • src/wp-admin/css/customize-controls.css

    diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css
    index cc5a37f08c..2d02a3cd38 100644
    a b body { 
    203203                    .15s border-color ease-in-out;
    204204}
    205205
    206 #customize-controls #customize-theme-controls .customize-themes-panel .accordion-section-title {
    207         color: #555;
    208         background-color: #fff;
    209         border-left: 4px solid #fff;
    210 }
    211 
    212206#customize-theme-controls .accordion-section-title:after {
    213207        content: "\f345";
    214208        color: #a0a5aa;
    body { 
    319313}
    320314
    321315#customize-theme-controls .customize-pane-child.open,
    322 #customize-theme-controls .customize-pane-child.current-panel,
    323 #customize-theme-controls .customize-themes-panel.customize-pane-child.current-panel {
     316#customize-theme-controls .customize-pane-child.current-panel {
    324317        -webkit-transform: none;
    325318        transform: none;
    326319}
    327320
    328 #customize-theme-controls .customize-themes-panel.customize-pane-child,
    329321.section-open #customize-theme-controls .customize-pane-parent,
    330322.in-sub-panel #customize-theme-controls .customize-pane-parent,
    331323.section-open #customize-info,
    332324.in-sub-panel #customize-info,
    333 .in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel,
    334 .in-themes-panel #customize-theme-controls .customize-pane-parent,
    335 .in-themes-panel #customize-info {
     325.in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel {
    336326        visibility: hidden;
    337327        height: 0;
    338328        overflow: hidden;
    body { 
    342332
    343333.section-open #customize-theme-controls .customize-pane-parent.busy,
    344334.in-sub-panel #customize-theme-controls .customize-pane-parent.busy,
    345 .in-themes-panel #customize-theme-controls .customize-pane-parent.busy,
    346335.section-open #customize-info.busy,
    347336.in-sub-panel #customize-info.busy,
    348 .in-themes-panel #customize-info.busy,
    349337.busy.section-open.in-sub-panel #customize-theme-controls .customize-pane-child.current-panel,
    350338#customize-theme-controls .customize-pane-child.open,
    351339#customize-theme-controls .customize-pane-child.current-panel,
    body { 
    355343        overflow: auto;
    356344}
    357345
    358 .in-themes-panel #customize-theme-controls .customize-pane-parent,
    359 .in-themes-panel #customize-info {
    360         -webkit-transform: translateX(100%);
    361         transform: translateX(100%);
    362 }
    363 
    364346#customize-theme-controls .customize-pane-child.accordion-section-content,
    365347#customize-theme-controls .customize-pane-child.accordion-sub-container {
    366348        display: block;
    p.customize-section-description { 
    12461228        100% { opacity: 1; }
    12471229}
    12481230
    1249 /* #customize-container is reused from customize-loader.js, hence the naming. */
    1250 .wp-customizer .customize-loading #customize-container {
     1231.wp-customizer .customize-loading #customize-themes-loading-container {
    12511232        display: block;
    12521233        -webkit-animation: customize-reload .75s; /* Can't use `transition` because `display` changes here. */
    12531234        animation: customize-reload .75s;
    12541235}
    12551236
    1256 #customize-theme-controls .control-section-themes .accordion-section-title:hover, /* Not a focusable element. */
    1257 #customize-theme-controls .control-section-themes .accordion-section-title {
     1237.customize-loading #customize-themes-loading-container span {
     1238    clear: both;
     1239    color: #555d66;
     1240    font-size: 18px;
     1241    font-style: normal;
     1242    margin: 0;
     1243    padding: 100px 0;
     1244    text-align: center;
     1245        width: 100%;
     1246        display: block;
     1247}
     1248
     1249.customize-loading #customize-themes-loading-container .customize-loading-text {
     1250        display: none;
     1251}
     1252
     1253#customize-theme-controls .control-panel-themes {
     1254        border-bottom: none;
     1255}
     1256
     1257#customize-theme-controls .control-panel-themes > .accordion-section-title:hover, /* Not a focusable element. */
     1258#customize-theme-controls .control-panel-themes > .accordion-section-title {
    12581259        cursor: default;
    12591260        background: #fff;
    12601261        color: #555d66;
    12611262        border-top: 1px solid #ddd;
    12621263        border-bottom: 1px solid #ddd;
    12631264        border-left: none;
    1264         margin-top: 0;
    1265 }
    1266 #customize-theme-controls .control-section-themes .customize-section-back {
    1267         position: absolute;
    1268         right: 0;
    1269         top: 0;
    1270         height: 80px;
    1271         border-left: 1px solid #ddd;
    1272         border-right: 4px solid #fff;
    1273 }
    1274 #customize-theme-controls .control-section-themes .customize-section-back:before {
    1275         content: "\f345";
    1276 }
    1277 #customize-theme-controls .control-section-themes .customize-section-back:hover,
    1278 #customize-theme-controls .control-section-themes .customize-section-back:focus {
    1279         border-right-color: #0073aa;
     1265        border-right: none;
     1266        margin: 0 0 15px 0;
     1267        padding-right: 100px; /* Space for the button */
    12801268}
    12811269
    12821270#customize-theme-controls .control-section-themes .customize-themes-panel .accordion-section-title:first-child:hover, /* Not a focusable element. */
    p.customize-section-description { 
    12841272        border-top: 0;
    12851273}
    12861274
    1287 #customize-theme-controls .control-section-themes > .accordion-section-title:hover, /* Not a focusable element. */
    1288 #customize-theme-controls .control-section-themes > .accordion-section-title {
    1289         margin: 0 0 15px;
    1290 }
    1291 
    1292 #customize-controls .customize-themes-panel .accordion-section-title:hover,
    1293 #customize-controls .customize-themes-panel .accordion-section-title {
    1294         margin: 15px -8px;
    1295 }
    1296 
    1297 #customize-controls .control-section-themes .accordion-section-title,
    1298 #customize-controls .customize-themes-panel .accordion-section-title {
    1299         padding-right: 100px; /* Space for the button */
    1300 }
    1301 
    1302 #customize-controls .control-section-themes .accordion-section-title span.customize-action,
     1275.control-panel-themes .accordion-section-title span.customize-action,
    13031276#customize-controls .customize-section-title span.customize-action {
    13041277        font-size: 13px;
    13051278        display: block;
    13061279        font-weight: 400;
    13071280}
    13081281
    1309 #customize-controls .control-section-themes .accordion-section-title .change-theme,
    1310 #customize-controls .customize-themes-panel .accordion-section-title .customize-theme {
     1282.control-panel-themes .accordion-section-title .change-theme {
    13111283        position: absolute;
    13121284        right: 10px;
    13131285        top: 50%;
    p.customize-section-description { 
    13151287        font-weight: 400;
    13161288}
    13171289
    1318 #customize-controls .control-section-themes .accordion-section-title:before {
     1290#customize-theme-controls .control-panel-themes > .accordion-section-title:after {
    13191291        display: none;
    13201292}
    13211293
    1322 #customize-controls .customize-themes-panel {
    1323         padding: 0 8px;
    1324         background: #f1f1f1;
    1325         box-sizing: border-box;
     1294.control-panel-themes .customize-themes-full-container {
     1295        position: fixed;
     1296        top: 0;
     1297        left: 0;
     1298        -webkit-transition: .18s left ease-in-out;
     1299        transition: .18s left ease-in-out;
     1300        margin: 46px 0 0 300px;
     1301        padding: 25px;
     1302        overflow-y: scroll;
     1303        width: calc(100% - 350px);
     1304        height: calc(100% - 96px);
     1305        background: #eee;
     1306        z-index: 20;
    13261307}
    13271308
    1328 #customize-controls .customize-themes-panel .accordion-section-title:first-child {
    1329         margin-top: 0;
     1309/* Animations for opening the themes panel */
     1310#customize-header-actions .save,
     1311#customize-header-actions .spinner,
     1312#customize-header-actions .customize-controls-preview-toggle {
     1313        position: relative;
     1314        top: 0;
     1315        -webkit-transition: .18s top ease-in-out;
     1316        transition: .18s top ease-in-out;
     1317}
     1318
     1319#customize-footer-actions,
     1320#customize-footer-actions .collapse-sidebar {
     1321        bottom: 0;
     1322        -webkit-transition: .18s bottom ease-in-out;
     1323        transition: .18s bottom ease-in-out;
     1324}
     1325
     1326.in-themes-panel:not(.animating) #customize-header-actions .save,
     1327.in-themes-panel:not(.animating) #customize-header-actions .spinner,
     1328.in-themes-panel:not(.animating) #customize-header-actions .customize-controls-preview-toggle,
     1329.in-themes-panel:not(.animating) #customize-preview,
     1330.in-themes-panel:not(.animating) #customize-footer-actions {
     1331        visibility: hidden;
     1332}
     1333
     1334.wp-full-overlay.in-themes-panel {
     1335        background: #eee; /* Prevents a black flash when fading in the panel */
    13301336}
    13311337
    1332 #customize-controls .customize-themes-panel .accordion-section-title:nth-child(2) {
     1338.in-themes-panel #customize-header-actions .save,
     1339.in-themes-panel #customize-header-actions .spinner,
     1340.in-themes-panel #customize-header-actions .customize-controls-preview-toggle {
     1341        top: -45px;
     1342}
     1343
     1344.in-themes-panel #customize-footer-actions,
     1345.in-themes-panel #customize-footer-actions .collapse-sidebar {
     1346        bottom: -45px;
     1347}
     1348
     1349/* Don't show the theme count while the panel opens, as it's in the wrong place during the animation */
     1350.in-themes-panel.animating .control-panel-themes .filter-themes-count {
     1351        display: none;
     1352}
     1353
     1354.in-themes-panel.wp-full-overlay .wp-full-overlay-sidebar-content {
     1355        bottom: 0;
     1356}
     1357
     1358.themes-filter-bar .feature-filter-toggle {
     1359        float: right;
     1360        margin: 3px 25px;
     1361}
     1362
     1363.themes-filter-bar .feature-filter-toggle:before {
     1364    content: "\f111";
     1365    margin: 0 5px 0 0;
     1366    font: normal 16px/1 dashicons;
     1367    vertical-align: text-bottom;
     1368    -webkit-font-smoothing: antialiased;
     1369    -moz-osx-font-smoothing: grayscale;
     1370}
     1371
     1372.themes-filter-bar .feature-filter-toggle.open {
     1373        background: #eee;
     1374        border-color: #999;
     1375        box-shadow: inset 0 2px 5px -3px rgba( 0, 0, 0, 0.5 );
     1376        -webkit-transform: translateY(1px);
     1377        transform: translateY(1px);
     1378}
     1379
     1380.themes-filter-bar .filter-drawer {
     1381        width: 100%;
     1382        position: absolute;
     1383        top: 46px;
     1384        border-top: 0;
     1385        margin: 0 -25px;
     1386        background: #eee;
     1387        border-bottom: 1px solid #ddd;
     1388}
     1389
     1390/* Adds a delay before fading in to avoid it "jumping" */
     1391@-webkit-keyframes themes-fade-in {
     1392        0% {
     1393                opacity: 0;
     1394        }
     1395        50% {
     1396                opacity: 0;
     1397        }
     1398        100% {
     1399                opacity: 1;
     1400        }
     1401}
     1402@keyframes themes-fade-in {
     1403        0% {
     1404                opacity: 0;
     1405        }
     1406        50% {
     1407                opacity: 0;
     1408        }
     1409        100% {
     1410                opacity: 1;
     1411        }
     1412}
     1413
     1414.control-panel-themes .customize-themes-full-container.animate {
     1415        -webkit-animation: .6s themes-fade-in 1;
     1416        animation: .6s themes-fade-in 1;
     1417}
     1418
     1419.in-themes-panel:not(.animating) .control-panel-themes .filter-themes-count {
     1420        -webkit-animation: .6s themes-fade-in 1;
     1421        animation: .6s themes-fade-in 1;
     1422}
     1423
     1424.control-panel-themes .filter-themes-count {
     1425        position: relative;
     1426        float: right;
     1427        line-height: 34px;
     1428}
     1429
     1430.control-panel-themes .filter-themes-count .themes-displayed {
     1431        font-weight: 600;
     1432        color: #555d66;
     1433}
     1434
     1435.control-panel-themes .filter-themes-count .see-themes,
     1436.control-panel-themes .filter-themes-count .filter-themes {
     1437        display: none;
     1438}
     1439
     1440
     1441/* Mobile - toggle between themes and filters */
     1442@media screen and (max-width:600px) {
     1443
     1444        /* Show a spinner in the filters view also, reusing the main customize spinner */
     1445        .in-themes-panel.loading #customize-header-actions .spinner {
     1446                position: fixed;
     1447                top: 0;
     1448                left: 48px;
     1449                visibility: visible;
     1450        }
     1451
     1452        .in-themes-panel.loading.showing-themes #customize-header-actions .spinner {
     1453                visibility: hidden;
     1454        }
     1455
     1456        .control-panel-themes .filter-themes-count {
     1457                width: -webkit-calc(100% - 93px);
     1458                width: calc(100% - 93px);
     1459        }
     1460
     1461        .control-panel-themes .filter-themes-count .themes-displayed {
     1462                display: none;
     1463        }
     1464
     1465        .wp-full-overlay:not(.showing-themes) .control-panel-themes .filter-themes-count .see-themes {
     1466                display: block;
     1467                float: right;
     1468        }
     1469
     1470        .wp-full-overlay.showing-themes .control-panel-themes .filter-themes-count .filter-themes {
     1471                display: block;
     1472                float: right;
     1473        }
     1474
     1475        .in-themes-panel.showing-themes .control-panel-themes .customize-panel-back {
     1476                position: fixed;
     1477                top: 0;
     1478                left: 0;
     1479                z-index: 10;
     1480                height: 45px;
     1481                background: #eee;
     1482        }
     1483
     1484        .in-themes-panel.showing-themes .control-panel-themes .customize-panel-back:before {
     1485                line-height: 45px;
     1486        }
     1487
     1488        .control-panel-themes .customize-themes-full-container {
     1489                width: -webkit-calc(100% - 50px);
     1490                width: calc(100% - 50px);
     1491                margin: 0;
     1492                top: 46px;
     1493                height: -webkit-calc(100% - 96px);
     1494                height: calc(100% - 96px);
     1495                z-index: 1;
     1496                display: none;
     1497        }
     1498
     1499        .showing-themes .control-panel-themes .customize-themes-full-container {
     1500                display: block;
     1501        }
     1502}
     1503
     1504.customize-themes-notifications {
     1505        margin: 0;
     1506}
     1507
     1508.control-panel-themes .customize-themes-notifications .notice {
     1509        margin: 0 0 25px 0;
     1510}
     1511
     1512.customize-themes-full-container .customize-themes-section {
     1513        display: none !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1514        overflow: hidden;
     1515}
     1516
     1517.customize-themes-full-container .customize-themes-section.current-section {
     1518        display: list-item !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1519}
     1520
     1521.control-section .customize-section-text-before {
     1522        padding: 0 0 8px 15px;
     1523        margin: 15px 0 0 0;
     1524        line-height: 16px;
     1525        border-bottom: 1px solid #ddd;
     1526        color: #555d66;
     1527}
     1528
     1529.control-panel-themes .customize-themes-section-title {
     1530        width: 100%;
     1531        background: #fff;
     1532        -webkit-box-shadow: none;
     1533        box-shadow: none;
     1534        outline: none;
     1535        border-top: none;
     1536        border-bottom: 1px solid #ddd;
     1537        border-left: 4px solid #fff;
     1538        border-right: none;
     1539        cursor: pointer;
     1540        padding: 10px 15px;
     1541        position: relative;
     1542        text-align: left;
    13331543        font-size: 14px;
    13341544        font-weight: 600;
     1545        color: #555d66;
     1546        text-shadow: none;
    13351547}
    13361548
    1337 #customize-controls .customize-themes-panel > h2 {
    1338         padding: 15px 8px 0 8px;
     1549.control-panel-themes .theme-section {
     1550        margin: 0;
     1551        position: relative;
    13391552}
    13401553
    1341 #customize-theme-controls .customize-themes-panel .accordion-section-content {
    1342         background: transparent;
    1343         display: block;
     1554.control-panel-themes .customize-themes-section-title:focus,
     1555.control-panel-themes .customize-themes-section-title:hover {
     1556        border-left-color: #0073aa;
     1557        color: #0073aa;
     1558        background: #f5f5f5;
    13441559}
    13451560
    1346 .customize-control.customize-control-theme {
    1347         margin-bottom: 8px;
     1561.control-panel-themes .theme-section .customize-themes-section-title.selected:after {
     1562        content: "\f147";
     1563        font: 16px/1 dashicons;
     1564        box-sizing: border-box;
     1565        width: 20px;
     1566        height: 20px;
     1567        padding: 3px 3px 1px 1px; /* Re-align the icon to the smaller grid */
     1568        -webkit-border-radius: 100%;
     1569        border-radius: 100%;
     1570        position: absolute;
     1571        top: 9px;
     1572        right: 15px;
     1573        background: #0073aa;
     1574        color: #fff;
     1575}
     1576
     1577.control-panel-themes .customize-themes-section-title.selected {
     1578        color: #0073aa;
    13481579}
    13491580
    13501581#customize-theme-controls .themes.accordion-section-content {
    p.customize-section-description { 
    13541585        width: 100%;
    13551586}
    13561587
    1357 .wp-customizer .theme-browser .themes {
    1358         padding-bottom: 8px;
     1588.loading .customize-themes-section .spinner {
     1589        display: block;
     1590        visibility: visible;
     1591        position: relative;
     1592        clear: both;
     1593        width: 20px;
     1594        height: 20px;
     1595        left: -webkit-calc(50% - 10px);
     1596        left: calc(50% - 10px);
     1597        float: none;
     1598        margin-top: 50px;
    13591599}
    13601600
    1361 .wp-customizer .theme-browser .theme {
    1362         margin: 0;
     1601.customize-themes-section .no-themes {
     1602        display: none;
     1603}
     1604
     1605.themes-section-installed_themes .theme .notice-success {
     1606        display: none; /* Hide "installed" notice on installed themes tab. */
     1607}
     1608
     1609.control-panel-themes .theme-browser .theme .theme-actions .button-primary {
     1610        margin: 0 0 0 8px;
     1611}
     1612
     1613.customize-control-theme .theme {
    13631614        width: 100%;
     1615        margin: 0;
     1616}
     1617
     1618.customize-control.customize-control-theme { /* override most properties on .customize-control */
     1619        -webkit-box-sizing: border-box;
     1620        -moz-box-sizing: border-box;
     1621        box-sizing: border-box;
     1622        width: 18.4%;
     1623        margin: 0 2% 2% 0;
     1624        padding: 0;
     1625        clear: none;
     1626}
     1627
     1628/* 5 columns above 2100px */
     1629@media screen and (min-width: 2101px) {
     1630        .customize-control.customize-control-theme:nth-child(5n) {
     1631                margin-right: 0;
     1632        }
     1633}
     1634
     1635/* 4 columns up to 2100px */
     1636@media screen and (min-width: 1601px) and (max-width: 2100px) {
     1637        .customize-control.customize-control-theme {
     1638                width: 23.5%;
     1639        }
     1640
     1641        .customize-control.customize-control-theme:nth-child(4n) {
     1642                margin-right: 0;
     1643        }
     1644}
     1645
     1646/* 3 columns up to 1600px */
     1647@media screen and (min-width: 1201px) and (max-width: 1600px) {
     1648        .customize-control.customize-control-theme {
     1649                width: 32%;
     1650        }
     1651
     1652        .customize-control.customize-control-theme:nth-child(3n) {
     1653                margin-right: 0;
     1654        }
     1655}
     1656
     1657/* 2 columns up to 1200px */
     1658@media screen and (min-width: 851px) and (max-width: 1200px) {
     1659        .customize-control.customize-control-theme {
     1660                width: 49%;
     1661        }
     1662
     1663        .customize-control.customize-control-theme:nth-child(even) {
     1664                margin-right: 0;
     1665        }
     1666}
     1667
     1668/* 1 column up to 850 px */
     1669@media screen and (max-width: 850px) {
     1670        .customize-control.customize-control-theme {
     1671                width: 100%;
     1672                margin: 0 0 3% 0;
     1673        }
     1674}
     1675
     1676.wp-customizer .theme-browser .themes {
     1677        padding-bottom: 8px;
    13641678}
    13651679
    13661680.wp-customizer .theme-browser .theme .theme-actions {
    1367         -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
    13681681        opacity: 1;
    13691682}
    13701683
    p.customize-section-description { 
    13761689        font-size: 32px;
    13771690}
    13781691
    1379 .wp-customizer #themes-filter {
    1380         font-size: 16px;
    1381         font-weight: 300;
    1382         line-height: 1.5;
    1383         width: 100%;
     1692.customize-preview-header.themes-filter-bar {
     1693        position: fixed;
     1694        top: 0;
     1695        left: 300px;
     1696        width: calc(100% - 300px);
     1697        height: 46px;
     1698        background: #fff;
     1699        z-index: 10;
     1700        padding: 6px 25px;
     1701        box-sizing: border-box;
     1702        border-bottom: 1px solid #ddd;
    13841703}
    13851704
    1386 .control-section-themes .accordion-section-title:after,
    1387 .customize-themes-panel .accordion-section-title:after {
    1388         display: none;
     1705.themes-filter-bar .themes-filter-container {
     1706        margin: 0;
     1707        padding: 0;
    13891708}
    13901709
    1391 .customize-themes-panel.control-panel-content {
    1392         border-top: 1px solid #ddd;
     1710.themes-filter-bar .wp-filter-search {
     1711        line-height: 25px;
     1712        padding: 3px 5px;
     1713        max-width: 100%;
     1714        width: 40%;
     1715        min-width: 300px;
     1716        position: absolute;
     1717        top: 6px;
     1718        left: 25px;
     1719}
     1720
     1721/* Unstick the filter bar on short windows/screens. This breakpoint is based on the
     1722   current length of .org feature filters assuming translations do not wrap lines. */
     1723@media screen and (max-height:540px) {
     1724        .customize-preview-header.themes-filter-bar {
     1725                position: relative;
     1726                left: 0;
     1727                width: 100%;
     1728                margin: 0 0 25px 0;
     1729        }
     1730        .wp-customizer .theme-browser .themes {
     1731                padding: 0 25px;
     1732                min-height: 540px;
     1733        }
     1734
     1735        .control-panel-themes .customize-themes-full-container {
     1736                margin-top: 0;
     1737                padding: 0;
     1738                height: 100%;
     1739                width: calc(100% - 300px);
     1740        }
    13931741}
    13941742
    13951743/* Details View */
    p.customize-section-description { 
    14061754        z-index: 109;
    14071755}
    14081756
     1757/* Avoid a z-index war by resetting elements that should be under the overlay.
     1758   This is likely required because of the way that sections and panels are positioned. */
     1759.wp-customizer.modal-open #customize-header-actions,
     1760.wp-customizer.modal-open .control-panel-themes .filter-themes-count,
     1761.wp-customizer.modal-open .control-panel-themes .customize-themes-section-title.selected:after {
     1762        z-index: -1;
     1763}
     1764
    14091765.wp-customizer .theme-overlay .theme-backdrop {
    14101766        background: rgba( 238, 238, 238, 0.75 );
    14111767        position: fixed;
    14121768        z-index: 110;
    14131769}
    14141770
     1771.wp-customizer .theme-overlay .star-rating {
     1772        float: left;
     1773        margin-right: 8px;
     1774}
     1775
     1776.wp-customizer .theme-rating .num-ratings {
     1777        line-height: 20px;
     1778}
     1779
    14151780.wp-customizer .theme-overlay .theme-wrap {
    14161781        left: 90px;
    14171782        right: 90px;
    14181783        top: 45px;
    14191784        bottom: 45px;
    14201785        z-index: 120;
    1421         max-width: 1740px; /* To ensure that theme screenshots are not displayed larger than 880px wide. */
    14221786}
    14231787
    14241788.wp-customizer .theme-overlay .theme-actions {
    1425         text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */
     1789        text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */
     1790        padding: 10px 15px;
    14261791}
    14271792
    1428 .ie8 .wp-customizer .theme-overlay .theme-header,
    1429 .ie8 .wp-customizer .theme-overlay .theme-about,
    1430 .ie8 .wp-customizer .theme-overlay .theme-actions {
    1431         position: static;
     1793.wp-customizer .theme-overlay .theme-actions .theme-install.preview {
     1794        margin-left: 8px;
     1795}
     1796
     1797.control-panel-themes .theme-actions .delete-theme {
     1798        left: 15px; /* these override themes.css on mobile */
     1799        right: auto;
     1800        bottom: auto;
     1801        position: absolute;
    14321802}
    14331803
     1804.modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content {
     1805        overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */
     1806}
     1807
     1808
    14341809/* Small Screens */
    14351810@media (max-width:850px), (max-height:472px) {
    14361811        .wp-customizer .theme-overlay .theme-wrap {
    body.cheatin { 
    14561831body.cheatin h1 {
    14571832        border-bottom: 1px solid #ddd;
    14581833        clear: both;
    1459         color: #666;
     1834        color: #555d66;
    14601835        font-size: 24px;
    14611836        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
    14621837        margin: 30px 0 0 0;
  • src/wp-admin/css/themes.css

    diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css
    index 6c4ce8716a..07294bc514 100644
    a b body.folded .theme-browser ~ .theme-overlay .theme-wrap { 
    549549        float: left;
    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}
    555555
    body.full-overlay-active { 
    17051705        display: none;
    17061706}
    17071707
    1708 #customize-container {
     1708#customize-container,
     1709#customize-themes-loading-container {
    17091710        display: none;
    17101711        background: #fff;
    17111712        z-index: 500000;
    body.full-overlay-active { 
    17201721
    17211722/* Make the Customizer and Theme installer overlays the only available content. */
    17221723#customize-container,
     1724#customize-themes-loading-container,
    17231725.theme-install-overlay {
    17241726        visibility: visible;
    17251727}
    body.full-overlay-active { 
    18241826
    18251827#customize-preview.wp-full-overlay-main:before,
    18261828.customize-loading #customize-container:before,
     1829.customize-loading #customize-themes-loading-container:before,
    18271830.theme-install-overlay .wp-full-overlay-main:before {
    18281831        content: "";
    18291832        display: block;
    body.full-overlay-active { 
    18611864
    18621865        #customize-preview.wp-full-overlay-main:before,
    18631866        .customize-loading #customize-container:before,
     1867        .customize-loading #customize-themes-loading-container:before,
    18641868        .theme-install-overlay .wp-full-overlay-main:before {
    18651869                background-image: url(../images/spinner-2x.gif);
    18661870        }
  • src/wp-admin/customize.php

    diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php
    index 81a7ae2741..3c5455720c 100644
    a b  
    109109?><title><?php echo $admin_title; ?></title>
    110110
    111111<script type="text/javascript">
    112 var ajaxurl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php', 'relative' ) ); ?>;
     112var ajaxurl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php', 'relative' ) ); ?>,
     113    pagenow = 'customize';
    113114</script>
    114115
    115116<?php
  • src/wp-admin/includes/theme.php

    diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php
    index c5de5e5c9c..632c1ac34d 100644
    a b function get_theme_feature_list( $api = true ) { 
    234234        // Hard-coded list is used if api not accessible.
    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' ),
     237                __( 'Subject' )  => array(
     238                        'blog'           => __( 'Blog' ),
     239                        'e-commerce'     => __( 'E-Commerce' ),
     240                        'education'      => __( 'Education' ),
     241                        'entertainment'  => __( 'Entertainment' ),
     242                        'food-and-drink' => __( 'Food & Drink' ),
     243                        'holiday'        => __( 'Holiday' ),
     244                        'news'           => __( 'News' ),
     245                        'photography'    => __( 'Photography' ),
     246                        'portfolio'      => __( 'Portfolio' ),
    245247                ),
    246248
    247249                __( 'Features' ) => array(
    248250                        'accessibility-ready'   => __( 'Accessibility Ready' ),
    249                         'buddypress'            => __( 'BuddyPress' ),
    250251                        'custom-background'     => __( 'Custom Background' ),
    251252                        'custom-colors'         => __( 'Custom Colors' ),
    252253                        'custom-header'         => __( 'Custom Header' ),
    253254                        'custom-logo'           => __( 'Custom Logo' ),
    254                         'custom-menu'           => __( 'Custom Menu' ),
    255255                        'editor-style'          => __( 'Editor Style' ),
    256256                        'featured-image-header' => __( 'Featured Image Header' ),
    257257                        'featured-images'       => __( 'Featured Images' ),
    258                         'flexible-header'       => __( 'Flexible Header' ),
    259258                        'footer-widgets'        => __( 'Footer Widgets' ),
    260                         'front-page-post-form'  => __( 'Front Page Posting' ),
    261259                        'full-width-template'   => __( 'Full Width Template' ),
    262                         'microformats'          => __( 'Microformats' ),
    263260                        'post-formats'          => __( 'Post Formats' ),
    264                         'rtl-language-support'  => __( 'RTL Language Support' ),
    265261                        'sticky-post'           => __( 'Sticky Post' ),
    266262                        'theme-options'         => __( 'Theme Options' ),
    267                         'threaded-comments'     => __( 'Threaded Comments' ),
    268                         'translation-ready'     => __( 'Translation Ready' ),
    269263                ),
    270264
    271                 __( 'Subject' )  => array(
    272                         'blog'           => __( 'Blog' ),
    273                         'e-commerce'     => __( 'E-Commerce' ),
    274                         'education'      => __( 'Education' ),
    275                         'entertainment'  => __( 'Entertainment' ),
    276                         'food-and-drink' => __( 'Food & Drink' ),
    277                         'holiday'        => __( 'Holiday' ),
    278                         'news'           => __( 'News' ),
    279                         'photography'    => __( 'Photography' ),
    280                         'portfolio'      => __( 'Portfolio' ),
     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
    284277        if ( ! $api || ! current_user_can( 'install_themes' ) )
    function wp_prepare_themes_for_js( $themes = null ) { 
    570563
    571564                $parent = false;
    572565                if ( $theme->parent() ) {
    573                         $parent = $theme->parent()->display( 'Name' );
    574                         $parents[ $slug ] = $theme->parent()->get_stylesheet();
     566                        $parent = $theme->parent();
     567                        $parents[ $slug ] = $parent->get_stylesheet();
     568                        $parent = $parent->display( 'Name' );
    575569                }
    576570
    577571                $customize_action = null;
    function wp_prepare_themes_for_js( $themes = null ) { 
    631625 * @since 4.2.0
    632626 */
    633627function customize_themes_print_templates() {
    634         $preview_url = esc_url( add_query_arg( 'theme', '__THEME__' ) ); // Token because esc_url() strips curly braces.
    635         $preview_url = str_replace( '__THEME__', '{{ data.id }}', $preview_url );
    636628        ?>
    637629        <script type="text/html" id="tmpl-customize-themes-details-view">
    638630                <div class="theme-backdrop"></div>
    function customize_themes_print_templates() { 
    644636                        </div>
    645637                        <div class="theme-about wp-clearfix">
    646638                                <div class="theme-screenshots">
    647                                 <# if ( data.screenshot[0] ) { #>
     639                                <# if ( data.screenshot && data.screenshot[0] ) { #>
    648640                                        <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div>
    649641                                <# } else { #>
    650642                                        <div class="screenshot blank"></div>
    function customize_themes_print_templates() { 
    657649                                        <# } #>
    658650                                        <h2 class="theme-name">{{{ data.name }}}<span class="theme-version"><?php printf( __( 'Version: %s' ), '{{ data.version }}' ); ?></span></h2>
    659651                                        <h3 class="theme-author"><?php printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' ); ?></h3>
    660                                         <p class="theme-description">{{{ data.description }}}</p>
     652
     653                                        <# if ( data.stars && 0 != data.num_ratings ) { #>
     654                                                <div class="theme-rating">
     655                                                        {{{ data.stars }}}
     656                                                        <span class="num-ratings"><?php echo sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ); ?></span>
     657                                                </div>
     658                                        <# } #>
     659
     660                                        <# if ( data.hasUpdate ) { #>
     661                                                <div class="notice notice-warning notice-alt notice-large" data-slug="{{ data.id }}">
     662                                                        <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
     663                                                        {{{ data.update }}}
     664                                                </div>
     665                                        <# } #>
    661666
    662667                                        <# if ( data.parent ) { #>
    663668                                                <p class="parent-theme"><?php printf( __( 'This is a child theme of %s.' ), '<strong>{{{ data.parent }}}</strong>' ); ?></p>
    664669                                        <# } #>
    665670
     671                                        <p class="theme-description">{{{ data.description }}}</p>
     672
    666673                                        <# if ( data.tags ) { #>
    667                                                 <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{ data.tags }}</p>
     674                                                <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
    668675                                        <# } #>
    669676                                </div>
    670677                        </div>
    671678
    672                         <# if ( ! data.active ) { #>
    673                                 <div class="theme-actions">
    674                                         <div class="inactive-theme">
    675                                                 <?php
    676                                                 /* translators: %s: Theme name */
    677                                                 $aria_label = sprintf( __( 'Preview %s' ), '{{ data.name }}' );
    678                                                 ?>
    679                                                 <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>
    680                                         </div>
    681                                 </div>
    682                         <# } #>
     679                        <div class="theme-actions">
     680                                <# if ( data.active ) { #>
     681                                        <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></a>
     682                                <# } else if ( 'installed' === data.type ) { #>
     683                                        <?php if ( current_user_can( 'delete_themes' ) ) { ?>
     684                                                <# if ( data.actions && data.actions['delete'] ) { #>
     685                                                        <a href="{{{ data.actions['delete'] }}}" data-slug="{{ data.id }}" class="button button-secondary delete-theme"><?php _e( 'Delete' ); ?></a>
     686                                                <# } #>
     687                                        <?php } ?>
     688                                        <button type="button" class="button button-primary preview-theme" data-slug="{{ data.id }}"><?php _e( 'Live Preview' ); ?></span>
     689                                <# } else { #>
     690                                        <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
     691                                        <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}"><?php _e( 'Install & Preview' ); ?></button>
     692                                <# } #>
     693                        </div>
    683694                </div>
    684695        </script>
    685696        <?php
  • src/wp-admin/js/customize-controls.js

    diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js
    index c80ef86e52..715407b8f4 100644
    a b  
    11061106                                section = this,
    11071107                                container = $( '#customize-theme-controls' );
    11081108
    1109                         // Watch for changes to the panel state
     1109                        // Watch for changes to the panel state.
    11101110                        inject = function ( panelId ) {
    11111111                                var parentContainer;
    11121112                                if ( panelId ) {
    1113                                         // The panel has been supplied, so wait until the panel object is registered
     1113                                        // The panel has been supplied, so wait until the panel object is registered.
    11141114                                        api.panel( panelId, function ( panel ) {
    1115                                                 // The panel has been registered, wait for it to become ready/initialized
     1115                                                // The panel has been registered, wait for it to become ready/initialized.
    11161116                                                panel.deferred.embedded.done( function () {
    11171117                                                        parentContainer = panel.contentContainer;
    11181118                                                        if ( ! section.headContainer.parent().is( parentContainer ) ) {
     
    11371137                                }
    11381138                        };
    11391139                        section.panel.bind( inject );
    1140                         inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
     1140                        inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
    11411141                },
    11421142
    11431143                /**
     
    13101310        /**
    13111311         * wp.customize.ThemesSection
    13121312         *
    1313          * Custom section for themes that functions similarly to a backwards panel,
    1314          * and also handles the theme-details view rendering and navigation.
     1313         * Custom section for themes that loads themes by category, and also
     1314         * handles the theme-details view rendering and navigation.
    13151315         *
    13161316         * @constructor
    13171317         * @augments wp.customize.Section
     
    13231323                template: '',
    13241324                screenshotQueue: null,
    13251325                $window: $( window ),
     1326                loaded: 0,
     1327                loading: false,
     1328                fullyLoaded: false,
     1329                term: '',
     1330                tags: '',
     1331                nextTerm: '',
     1332                nextTags: '',
     1333                headerContainer: $(),
    13261334
    13271335                /**
    1328                  * @since 4.2.0
     1336                 * Embed the section in the DOM when the themes panel is ready.
     1337                 *
     1338                 * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
     1339                 *
     1340                 * @since 4.9.0
    13291341                 */
    1330                 initialize: function () {
    1331                         this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
    1332                         return api.Section.prototype.initialize.apply( this, arguments );
     1342                embed: function () {
     1343                        var inject,
     1344                                section = this,
     1345                                container = $( '#customize-theme-controls' );
     1346
     1347                        // Watch for changes to the panel state
     1348                        inject = function ( panelId ) {
     1349                                var parentContainer;
     1350                                api.panel( panelId, function ( panel ) {
     1351                                        // The panel has been registered, wait for it to become ready/initialized
     1352                                        panel.deferred.embedded.done( function () {
     1353                                                parentContainer = panel.contentContainer;
     1354                                                if ( ! section.headContainer.parent().is( parentContainer ) ) {
     1355                                                        parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
     1356                                                }
     1357                                                if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
     1358                                                        container.append( section.contentContainer );
     1359                                                }
     1360                                                section.deferred.embedded.resolve();
     1361                                        });
     1362                                } );
     1363                        };
     1364                        section.panel.bind( inject );
     1365                        inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
    13331366                },
    13341367
    13351368                /**
     
    13581391
    13591392                                // Pressing the escape key fires a theme:collapse event
    13601393                                if ( 27 === event.keyCode ) {
    1361                                         section.closeDetails();
     1394                                        if ( $( 'body' ).hasClass( 'modal-open' ) ) {
     1395                                                // Escape from the details modal.
     1396                                                section.closeDetails();
     1397                                        } else {
     1398                                                // Escape from the inifinite scroll list.
     1399                                                section.headerContainer.find( '.customize-themes-section-title' ).focus();
     1400                                        }
    13621401                                        event.stopPropagation(); // Prevent section from being collapsed.
    13631402                                }
    13641403                        });
    13651404
    1366                         _.bindAll( this, 'renderScreenshots' );
     1405                        _.bindAll( this, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
    13671406                },
    13681407
    13691408                /**
    13701409                 * Override Section.isContextuallyActive method.
    13711410                 *
    13721411                 * Ignore the active states' of the contained theme controls, and just
    1373                  * use the section's own active state instead. This ensures empty search
    1374                  * results for themes to cause the section to become inactive.
     1412                 * use the section's own active state instead. This prevents empty search
     1413                 * results for theme sections from causing the section to become inactive.
    13751414                 *
    13761415                 * @since 4.2.0
    13771416                 *
     
    13961435                                section.collapse();
    13971436                        });
    13981437
    1399                         // Expand/Collapse section/panel.
    1400                         section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
    1401                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1402                                         return;
     1438                        section.headerContainer = $( '#accordion-section-' + section.id );
     1439
     1440                        // Expand section/panel. Only collapse when opening another section.
     1441                        section.headerContainer.on( 'click', '.customize-themes-section-title', function() {
     1442
     1443                                // Toggle accordion filters under section headers.
     1444                                if ( section.headerContainer.find( '.filter-details' ).length ) {
     1445                                        section.headerContainer.find( '.customize-themes-section-title' )
     1446                                                .toggleClass( 'details-open' )
     1447                                                .attr('aria-expanded', function ( i, attr ) {
     1448                                                        return attr === 'true' ? 'false' : 'true';
     1449                                                });
     1450                                        section.headerContainer.find( '.filter-details' ).slideToggle( 180 );
    14031451                                }
    1404                                 event.preventDefault(); // Keep this AFTER the key filter above
    14051452
    1406                                 if ( section.expanded() ) {
    1407                                         section.collapse();
    1408                                 } else {
     1453                                // Open the section.
     1454                                if ( ! section.expanded() ) {
    14091455                                        section.expand();
    14101456                                }
    14111457                        });
    14121458
    1413                         // Theme navigation in details view.
    1414                         section.container.on( 'click keydown', '.left', function( event ) {
    1415                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1416                                         return;
    1417                                 }
     1459                        // Preview installed themes.
     1460                        section.container.on( 'click', '.theme-actions .preview-theme', function() {
     1461                                var themeId = $( this ).data( 'slug' );
    14181462
    1419                                 event.preventDefault(); // Keep this AFTER the key filter above
     1463                                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     1464                                api.panel( 'themes' ).loadThemePreview( themeId ).fail( function() {
     1465                                        $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     1466                                } );
     1467                        });
    14201468
     1469                        // Theme navigation in details view.
     1470                        section.container.on( 'click', '.left', function() {
    14211471                                section.previousTheme();
    14221472                        });
    14231473
    1424                         section.container.on( 'click keydown', '.right', function( event ) {
    1425                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1426                                         return;
    1427                                 }
    1428 
    1429                                 event.preventDefault(); // Keep this AFTER the key filter above
    1430 
     1474                        section.container.on( 'click', '.right', function() {
    14311475                                section.nextTheme();
    14321476                        });
    14331477
    1434                         section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
    1435                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1436                                         return;
    1437                                 }
    1438 
    1439                                 event.preventDefault(); // Keep this AFTER the key filter above
    1440 
     1478                        section.container.on( 'click', '.theme-backdrop, .close', function() {
    14411479                                section.closeDetails();
    14421480                        });
    14431481
    14441482                        var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
    1445                         section.container.on( 'input', '#themes-filter', function( event ) {
     1483
     1484                        // Filter-search all theme objects loaded in the section.
     1485                        section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
    14461486                                var count,
    14471487                                        term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
    14481488                                        controls = section.controls();
     
    14541494                                renderScreenshots();
    14551495
    14561496                                // Update theme count.
    1457                                 count = section.container.find( 'li.customize-control:visible' ).length;
    1458                                 section.container.find( '.theme-count' ).text( count );
     1497                                count = section.contentContainer.find( 'li.customize-control:visible' ).length;
     1498                                section.updateCount( count );
    14591499                        });
    14601500
    1461                         // Pre-load the first 3 theme screenshots.
    1462                         api.bind( 'ready', function () {
    1463                                 _.each( section.controls().slice( 0, 3 ), function ( control ) {
    1464                                         var img, src = control.params.theme.screenshot[0];
    1465                                         if ( src ) {
    1466                                                 img = new Image();
    1467                                                 img.src = src;
     1501                        // Event listeners for remote wporg queries with user-entered terms.
     1502                        if ( 'wporg' === section.params.action ) {
     1503                               
     1504                                // Search terms.
     1505                                var debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
     1506                                section.contentContainer.on( 'input', '#wp-filter-search-input', function() {
     1507                                        debounced( section );
     1508                                        if ( ! section.expanded() ) {
     1509                                                section.expand();
    14681510                                        }
     1511                                        section.checkTerm( section );
     1512                                });
     1513
     1514                                // Feature filters.
     1515                                section.contentContainer.on( 'click', '.filter-group input', function() {
     1516                                        section.filtersChecked();
     1517                                        section.checkTerm( section );
    14691518                                });
     1519
     1520                                // Toggle feature filter sections.
     1521                                section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
     1522                                        $( e.currentTarget )
     1523                                                .toggleClass( 'open' )
     1524                                                .attr('aria-expanded', function ( i, attr ) {
     1525                                                        return attr === 'true' ? 'false' : 'true';
     1526                                                })
     1527                                                .next( '.filter-drawer' ).slideToggle( 180 );
     1528                                });
     1529                        }
     1530
     1531                        // Move section controls to the themes area.
     1532                        api.bind( 'ready', function () {
     1533                                section.contentContainer = section.container.find( '.customize-themes-section' );
     1534                                section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
     1535                                section.container.add( section.headerContainer );
    14701536                        });
    14711537                },
    14721538
     
    14781544                 * @param {Boolean}  expanded
    14791545                 * @param {Object}   args
    14801546                 * @param {Boolean}  args.unchanged
    1481                  * @param {Callback} args.completeCallback
     1547                 * @param {Function} args.completeCallback
    14821548                 */
    14831549                onChangeExpanded: function ( expanded, args ) {
    14841550
     
    14911557                        }
    14921558
    14931559                        // Note: there is a second argument 'args' passed
    1494                         var panel = this,
    1495                                 section = panel.contentContainer,
    1496                                 overlay = section.closest( '.wp-full-overlay' ),
    1497                                 container = section.closest( '.wp-full-overlay-sidebar-content' ),
    1498                                 customizeBtn = section.find( '.customize-theme' ),
    1499                                 changeBtn = panel.headContainer.find( '.change-theme' );
     1560                        var section = this,
     1561                                container = section.contentContainer.closest( '.customize-themes-full-container' );
     1562
     1563                        if ( expanded ) {
     1564
     1565                                // Try to load controls if none are loaded yet.
     1566                                if ( 0 === section.loaded ) {
     1567                                        section.loadControls();
     1568                                }
    15001569
    1501                         if ( expanded && ! section.hasClass( 'current-panel' ) ) {
    15021570                                // Collapse any sibling sections/panels
    15031571                                api.section.each( function ( otherSection ) {
    1504                                         if ( otherSection !== panel ) {
     1572                                        if ( otherSection !== section ) {
    15051573                                                otherSection.collapse( { duration: args.duration } );
    15061574                                        }
    15071575                                });
    1508                                 api.panel.each( function ( otherPanel ) {
    1509                                         otherPanel.collapse( { duration: 0 } );
    1510                                 });
    15111576
    1512                                 panel._animateChangeExpanded( function() {
    1513                                         changeBtn.attr( 'tabindex', '-1' );
    1514                                         customizeBtn.attr( 'tabindex', '0' );
     1577                                section.contentContainer.addClass( 'current-section' );
     1578                                container.scrollTop();
     1579                                section.headerContainer.find( '.customize-themes-section-title' ).addClass( 'selected' ).attr( 'aria-expanded', 'true' );
    15151580
    1516                                         customizeBtn.focus();
    1517                                         section.css( 'top', '' );
    1518                                         container.scrollTop( 0 );
     1581                                container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
     1582                                container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
    15191583
    1520                                         if ( args.completeCallback ) {
    1521                                                 args.completeCallback();
     1584                                if ( args.completeCallback ) {
     1585                                        args.completeCallback();
     1586                                }
     1587                                section.updateCount(); // Show this section's count.
     1588                        } else {
     1589                                section.contentContainer.removeClass( 'current-section' );
     1590
     1591                                // Always hide, even if they don't exist or are already hidden.
     1592                                section.headerContainer.find( '.customize-themes-section-title' ).removeClass( 'selected details-open' ).attr( 'aria-expanded', 'false' );
     1593                                section.headerContainer.find( '.filter-details' ).slideUp( 180 );
     1594
     1595                                container.off( 'scroll' );
     1596
     1597                                if ( args.completeCallback ) {
     1598                                        args.completeCallback();
     1599                                }
     1600                        }
     1601                },
     1602
     1603                /**
     1604                 * Return the section's content element without detachng from the parent.
     1605                 *
     1606                 * @since 4.9.0
     1607                 */
     1608                getContent: function() {
     1609                        return this.container.find( '.control-section-content' );
     1610                },
     1611
     1612                /**
     1613                 * Load theme data via Ajax and add themes to the section as controls.
     1614                 *
     1615                 * @since 4.9.0
     1616                 */
     1617                loadControls: function() {
     1618                        var section = this, params, page, request;
     1619
     1620                        if ( section.loading ) {
     1621                                return; // We're already loading a batch of themes.
     1622                        }
     1623
     1624                        // Parameters for every API query. Additional params are set in PHP.
     1625                        page = Math.ceil( section.loaded / 100 ) + 1;
     1626                        params = {
     1627                                'switch-themes-nonce': api.settings.nonce['switch-themes'],
     1628                                'wp_customize': 'on',
     1629                                'theme_action': section.params.action,
     1630                                'customized_theme': api.settings.theme.stylesheet,
     1631                                'page': page
     1632                        };
     1633
     1634                        // Add fields for wporg actions.
     1635                        if ( 'wporg' === section.params.action ) {
     1636                                        params.search = section.term;
     1637                                        params.tags = section.tags;
     1638                        }
     1639
     1640                        // Load themes.
     1641                        section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
     1642                        section.loading = true;
     1643                        section.container.find( '.no-themes' ).hide();
     1644                        request = wp.ajax.post( 'customize-load-themes', params );
     1645                        request.done(function( data ) {
     1646                                var themes = data.themes,
     1647                                    themeControl, newThemeControls;
     1648
     1649                                // Stop and try again if the term changed while loading.
     1650                                if ( section.nextTerm || section.nextTags ) {
     1651                                        if ( section.nextTerm ) {
     1652                                                section.term = section.nextTerm;
    15221653                                        }
    1523                                 } );
     1654                                        if ( section.nextTags ) {
     1655                                                section.tags = section.nextTags;
     1656                                        }
     1657                                        section.nextTerm = '';
     1658                                        section.nextTags = '';
     1659                                        section.loading = false;
     1660                                        section.loadControls();
     1661                                        return;
     1662                                }
    15241663
    1525                                 overlay.addClass( 'in-themes-panel' );
    1526                                 section.addClass( 'current-panel' );
    1527                                 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
    1528                                 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
     1664                                if ( 0 !== themes.length ) {
     1665                                        newThemeControls = [];
     1666                                        // Add controls for each theme.
     1667                                        _.each( themes, function ( theme ) {
     1668                                                var customizeId = section.params.action + '_theme_' + theme.id;
     1669                                                themeControl = new api.controlConstructor.theme( customizeId, {
     1670                                                        params: {
     1671                                                                type: 'theme',
     1672                                                                content: '<li id="customize-control-theme-' + section.params.action + '_' + theme.id + '" class="customize-control customize-control-theme"></li>',
     1673                                                                section: section.params.id,
     1674                                                                active: true,
     1675                                                                theme: theme,
     1676                                                                priority: section.loaded + 1
     1677                                                        },
     1678                                                        previewer: api.previewer
     1679                                                } );
    15291680
    1530                         } else if ( ! expanded && section.hasClass( 'current-panel' ) ) {
    1531                                 panel._animateChangeExpanded( function() {
    1532                                         changeBtn.attr( 'tabindex', '0' );
    1533                                         customizeBtn.attr( 'tabindex', '-1' );
     1681                                                api.control.add( customizeId, themeControl );
     1682                                                newThemeControls.push( themeControl );
     1683                                                section.loaded = section.loaded + 1;
     1684                                        });
    15341685
    1535                                         changeBtn.focus();
    1536                                         section.css( 'top', '' );
     1686                                        if ( 1 === page ) {
     1687                                                // Pre-load the first 3 theme screenshots.
     1688                                                _.each( section.controls().slice( 0, 3 ), function ( control ) {
     1689                                                        var img, src = control.params.theme.screenshot[0];
     1690                                                        if ( src ) {
     1691                                                                img = new Image();
     1692                                                                img.src = src;
     1693                                                        }
     1694                                                });
     1695                                                if ( 'installed' !== section.params.action ) {
     1696                                                        wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
     1697                                                }
     1698                                        } else {
     1699                                                Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
     1700                                        }
     1701                                        _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
    15371702
    1538                                         if ( args.completeCallback ) {
    1539                                                 args.completeCallback();
     1703                                        if ( 'installed' === section.params.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
     1704                                                section.fullyLoaded = true;
    15401705                                        }
    1541                                 } );
     1706                                } else {
     1707                                        if ( 0 === section.loaded ) {
     1708                                                section.container.find( '.no-themes' ).show();
     1709                                                wp.a11y.speak( section.container.find( '.no-themes' ).text() );
     1710                                        } else {
     1711                                                section.fullyLoaded = true;
     1712                                        }
     1713                                }
     1714                                if ( 'installed' === section.params.action ) {
     1715                                        section.updateCount();
     1716                                } else {
     1717                                        section.updateCount( data.info.results );
     1718                                }
     1719                                section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
     1720
     1721                                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1722                                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1723                                section.loading = false;
     1724                        });
     1725                        request.fail(function( data ) {
     1726                                if ( 'undefined' === typeof data ) {
     1727                                        section.container.find( '.unexpected-error' ).show();
     1728                                        wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
     1729                                } else if ( typeof console !== 'undefined' && console.error ) {
     1730                                        console.error( data );
     1731                                }
     1732
     1733                                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1734                                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1735                                section.loading = false;
     1736                        });
     1737                },
     1738
     1739                /**
     1740                 * Determines whether more themes should be loaded, and loads them.
     1741                 *
     1742                 * @since 4.9.0
     1743                 */
     1744                loadMore: function() {
     1745                        var section = this, container, bottom, threshold;
     1746                        if ( ! section.fullyLoaded && ! section.loading ) {
     1747                                container = section.container.closest( '.customize-themes-full-container' );
     1748
     1749                                bottom = container.scrollTop() + container.height();
     1750                                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.
    15421751
    1543                                 overlay.removeClass( 'in-themes-panel' );
    1544                                 section.removeClass( 'current-panel' );
    1545                                 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
     1752                                if ( bottom > threshold ) {
     1753                                        section.loadControls();
     1754                                }
     1755                        }
     1756                },
     1757
     1758                /**
     1759                 * Event handler for search input that determines if the terms have changed and loads new controls as needed.
     1760                 *
     1761                 * @since 4.9.0
     1762                 *
     1763                 * @param {wp.customize.ThemesSection} section The current theme section, passed through the debouncer.
     1764                 */
     1765                checkTerm: function( section ) {
     1766                        var newTerm;
     1767
     1768                        // Find term.
     1769                        if ( 'wporg' === section.params.action ) {
     1770                                newTerm = $( '#wp-filter-search-input' ).val();
     1771                        } else {
     1772                                return;
     1773                        }
     1774
     1775                        if ( section.term === newTerm ) {
     1776                                return;
     1777                        } else {
     1778                                section.initializeNewQuery( newTerm, section.tags );
     1779                        };
     1780
     1781                },
     1782
     1783                /**
     1784                 * Check for filters checked in the feature filter list and initialize a new query.
     1785                 *
     1786                 * @since 4.9.0
     1787                 */
     1788                filtersChecked: function() {
     1789                        var section = this,
     1790                            items = section.container.find( '.filter-group' ).find( ':checkbox' ),
     1791                            tags = [];
     1792
     1793                        _.each( items.filter( ':checked' ), function( item ) {
     1794                                tags.push( $( item ).prop( 'value' ) );
     1795                        });
     1796
     1797                        // When no filters are checked, restore initial state.
     1798                        if ( tags.length === 0 ) {
     1799                                tags = '';
     1800                        }
     1801
     1802                        section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
     1803
     1804                        section.initializeNewQuery( section.term, tags )
     1805                },
     1806
     1807                /**
     1808                 * Reset the current query and load new results.
     1809                 *
     1810                 * @since 4.9.0
     1811                 */
     1812                initializeNewQuery: function( newTerm, newTags ) {
     1813                        var section = this;
     1814
     1815                        // Clear the controls in the section.
     1816                        _.each( section.controls(), function( control ) {
     1817                                control.container.remove();
     1818                                api.control.remove( control.id );
     1819                        });
     1820                        section.loaded = 0;
     1821                        section.fullyLoaded = false;
     1822                        section.screenshotQueue = null;
     1823
     1824                        // Run a new query, with loadControls handling paging, etc.
     1825                        if ( ! section.loading ) {
     1826                                section.term = newTerm;
     1827                                section.tags = newTags;
     1828                                section.loadControls();
     1829                        } else {
     1830                                section.nextTerm = newTerm; // This will reload from loadControls() with the newest term once the current batch is loaded.
     1831                                section.nextTags = newTags; // This will reload from loadControls() with the newest tags once the current batch is loaded.
     1832                        }
     1833                        if ( ! section.expanded() ) {
     1834                                section.expand(); // Expand the section if it isn't expanded.
    15461835                        }
    15471836                },
    15481837
     
    15541843                renderScreenshots: function( ) {
    15551844                        var section = this;
    15561845
    1557                         // Fill queue initially.
    1558                         if ( section.screenshotQueue === null ) {
    1559                                 section.screenshotQueue = section.controls();
     1846                        // Fill queue initially, or check for more if empty.
     1847                        if ( section.screenshotQueue === null || 0 === section.screenshotQueue.length ) {
     1848                                // Add controls that haven't had their screenshots rendered.
     1849                                section.screenshotQueue = _.filter( section.controls(), function( control ) {
     1850                                        return ! control.screenshotRendered;
     1851                                });
    15601852                        }
    15611853
    1562                         // Are all screenshots rendered?
     1854                        // Are all screenshots rendered (for now)?
    15631855                        if ( ! section.screenshotQueue.length ) {
    15641856                                return;
    15651857                        }
     
    15951887                },
    15961888
    15971889                /**
     1890                 * Update the number of themes in the section.
     1891                 *
     1892                 * @since 4.9.0
     1893                 */
     1894                updateCount: function ( count ) {
     1895                        if ( ! count ) {
     1896                                count = this.loaded;
     1897                        }
     1898
     1899                        var displayed = this.contentContainer.find( '.themes-displayed' ),
     1900                            countEl = this.contentContainer.find( '.theme-count' );
     1901
     1902                        if ( 0 === count ) {
     1903                                countEl.text( count );
     1904                        } else {
     1905                                // Animate the count change for emphasis.
     1906                                displayed.fadeOut( 180, function() {
     1907                                        countEl.text( count );
     1908                                        displayed.fadeIn( 180 );
     1909                                } );
     1910                                wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
     1911                        }
     1912                },
     1913
     1914                /**
    15981915                 * Advance the modal to the next theme.
    15991916                 *
    16001917                 * @since 4.2.0
     
    16141931                 * @since 4.2.0
    16151932                 */
    16161933                getNextTheme: function () {
    1617                         var control, next;
    1618                         control = api.control( 'theme_' + this.currentTheme );
     1934                        var section = this, control, next;
     1935                        control = api.control( section.params.action + '_theme_' + this.currentTheme );
    16191936                        next = control.container.next( 'li.customize-control-theme' );
    16201937                        if ( ! next.length ) {
    16211938                                return false;
    16221939                        }
    1623                         next = next[0].id.replace( 'customize-control-', '' );
     1940                        next = next[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    16241941                        control = api.control( next );
    16251942
    16261943                        return control.params.theme;
     
    16461963                 * @since 4.2.0
    16471964                 */
    16481965                getPreviousTheme: function () {
    1649                         var control, previous;
    1650                         control = api.control( 'theme_' + this.currentTheme );
     1966                        var section = this, control, previous;
     1967                        control = api.control( section.params.action + '_theme_' + this.currentTheme );
    16511968                        previous = control.container.prev( 'li.customize-control-theme' );
    16521969                        if ( ! previous.length ) {
    16531970                                return false;
    16541971                        }
    1655                         previous = previous[0].id.replace( 'customize-control-', '' );
     1972                        previous = previous[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    16561973                        control = api.control( previous );
    16571974
    16581975                        return control.params.theme;
     
    17342051                 * @param {Object}   theme
    17352052                 */
    17362053                showDetails: function ( theme, callback ) {
    1737                         var section = this, link;
     2054                        var section = this;
    17382055                        callback = callback || function(){};
    17392056                        section.currentTheme = theme.id;
    17402057                        section.overlay.html( section.template( theme ) )
     
    17432060                        $( 'body' ).addClass( 'modal-open' );
    17442061                        section.containFocus( section.overlay );
    17452062                        section.updateLimits();
    1746 
    1747                         link = section.overlay.find( '.inactive-theme > a' );
    1748 
    1749                         link.on( 'click', function( event ) {
    1750                                 event.preventDefault();
    1751 
    1752                                 // Short-circuit if request is currently being made.
    1753                                 if ( link.hasClass( 'disabled' ) ) {
    1754                                         return;
    1755                                 }
    1756                                 link.addClass( 'disabled' );
    1757 
    1758                                 section.loadThemePreview( theme.id ).fail( function() {
    1759                                         link.removeClass( 'disabled' );
    1760                                 } );
    1761                         } );
     2063                        wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
    17622064                        callback();
    17632065                },
    17642066
     
    17702072                closeDetails: function () {
    17712073                        $( 'body' ).removeClass( 'modal-open' );
    17722074                        this.overlay.fadeOut( 'fast' );
    1773                         api.control( 'theme_' + this.currentTheme ).focus();
     2075                        api.control( this.params.action + '_theme_' + this.currentTheme ).container.find( '.theme' ).focus();
    17742076                },
    17752077
    17762078                /**
     
    18502152                        }
    18512153                        if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
    18522154                                container.append( panel.contentContainer );
    1853                                 panel.renderContent();
    18542155                        }
     2156                        panel.renderContent();
    18552157
    18562158                        panel.deferred.embedded.resolve();
    18572159                },
     
    20612363                }
    20622364        });
    20632365
     2366
     2367        /**
     2368         * wp.customize.ThemesPanel
     2369         *
     2370         * Custom section for themes that displays without the customize preview.
     2371         *
     2372         * @constructor
     2373         * @augments wp.customize.Panel
     2374         * @augments wp.customize.Container
     2375         */
     2376        api.ThemesPanel = api.Panel.extend({
     2377                installingThemes: [],
     2378
     2379                /**
     2380                 * @since 4.9.0
     2381                 */
     2382                attachEvents: function () {
     2383                        var panel = this;
     2384
     2385                        // Attach regular panel events.
     2386                        api.Panel.prototype.attachEvents.apply( this );
     2387
     2388                        // Collapse panel to customize the current theme.
     2389                        panel.contentContainer.on( 'click', '.customize-theme', function() {
     2390                                panel.collapse();
     2391                        });
     2392
     2393                        // Toggle between filtering and browsing themes on mobile.
     2394                        panel.contentContainer.on( 'click', '.see-themes, .filter-themes', function() {
     2395                                $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
     2396                        });
     2397
     2398                        // Install (and maybe preview) a theme.
     2399                        panel.contentContainer.on( 'click', '.theme-install', function( event ) {
     2400                                panel.installTheme( event );
     2401                        });
     2402
     2403                        // Update a theme. Theme cards have the class, the details modal has the id.
     2404                        panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
     2405                                // #update-theme is a link.
     2406                                event.preventDefault();
     2407                                event.stopPropagation();
     2408
     2409                                panel.updateTheme( event );
     2410                        });
     2411
     2412                        // Delete a theme.
     2413                        panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
     2414                                panel.deleteTheme( event );
     2415                        });
     2416
     2417                        _.bindAll( this, 'installTheme', 'updateTheme' );
     2418                },
     2419
     2420                /**
     2421                 * Update UI to reflect expanded state
     2422                 *
     2423                 * @since 4.9.0
     2424                 *
     2425                 * @param {Boolean}  expanded
     2426                 * @param {Object}   args
     2427                 * @param {Boolean}  args.unchanged
     2428                 * @param {Function} args.completeCallback
     2429                 */
     2430                onChangeExpanded: function ( expanded, args ) {
     2431
     2432                        // Expand/collapse the panel normally.
     2433                        api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
     2434
     2435                        // Immediately call the complete callback if there were no changes
     2436                        if ( args.unchanged ) {
     2437                                if ( args.completeCallback ) {
     2438                                        args.completeCallback();
     2439                                }
     2440                                return;
     2441                        }
     2442
     2443                        // Note: there is a second argument 'args' passed
     2444                        var panel = this,
     2445                                overlay = panel.headContainer.closest( '.wp-full-overlay' );
     2446
     2447                        if ( expanded ) {
     2448                                overlay
     2449                                        .addClass( 'in-themes-panel' ).addClass( 'showing-themes' )
     2450                                        .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
     2451
     2452                                // Automatically open the installed themes section.
     2453                                api.section( 'installed_themes' ).expand();
     2454                        } else {
     2455                                overlay
     2456                                        .removeClass( 'in-themes-panel' )
     2457                                        .find( '.customize-themes-full-container' ).removeClass( 'animate' );
     2458                        }
     2459                },
     2460
     2461                /**
     2462                 * Install a theme via wp.updates.
     2463                 *
     2464                 * @since 4.9.0
     2465                 */
     2466                installTheme: function( event ) {
     2467                        var panel = this, preview = false, slug = $( event.target ).data( 'slug' );
     2468
     2469                        if ( -1 !== $.inArray( this.installingThemes, slug ) ) {
     2470                                return; // Theme is already being installed.
     2471                        }
     2472
     2473                        wp.updates.maybeRequestFilesystemCredentials( event );
     2474
     2475                        $( document ).one( 'wp-theme-install-success', function( event, response ) {
     2476                                var theme = false, customizeId, themeControl;
     2477                                if ( preview ) {
     2478
     2479                                        // Update loading message. Everything else is handled by reloading the page.
     2480                                //      $( '#customize-themes-loading-container span' ).hide();
     2481                                //      $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' );
     2482
     2483                                        panel.loadThemePreview( slug ).fail( function() {
     2484                                                $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     2485                                        } );
     2486
     2487                                } else {
     2488                                        api.control.each( function( control ) {
     2489                                                if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2490                                                        theme = control.params.theme; // Used below to add theme control.
     2491                                                        control.rerenderAsInstalled( true );
     2492                                                }
     2493                                        });
     2494
     2495                                        // Don't add the same theme more than once.
     2496                                        if ( ! theme || 'undefined' !== typeof api.control( 'installed_theme_' + theme.id ) ) {
     2497                                                return;
     2498                                        }
     2499
     2500                                        // Add theme control to installed section.
     2501                                        theme.type = 'installed';
     2502                                        customizeId = 'installed_theme_' + theme.id;
     2503                                        themeControl = new api.controlConstructor.theme( customizeId, {
     2504                                                params: {
     2505                                                        type: 'theme',
     2506                                                        content: $( '<li class="customize-control customize-control-theme"></li>' ).attr( 'id', 'customize-control-theme-installed_' + theme.id ).prop( 'outerHTML' ),
     2507                                                        section: 'installed_themes',
     2508                                                        active: true,
     2509                                                        theme: theme,
     2510                                                        priority: 0 // Add all newly-installed themes to the top.
     2511                                                },
     2512                                                previewer: api.previewer
     2513                                        } );
     2514
     2515                                        api.control.add( customizeId, themeControl );
     2516                                        api.control( customizeId ).container.trigger( 'render-screenshot' );
     2517
     2518                                        // Close the details modal if it's open to the installed theme.
     2519                                        api.section.each( function( section ) {
     2520                                                if ( 'themes' === section.params.type ) {
     2521                                                        if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
     2522                                                                section.closeDetails();
     2523                                                        }
     2524                                                }
     2525                                        });
     2526                                }
     2527                        } );
     2528
     2529                        this.installingThemes.push( $( event.target ).data( 'slug' ) ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
     2530                        wp.updates.installTheme( {
     2531                                slug: slug
     2532                        } );
     2533
     2534                        // Also preview the theme as the event is triggered on Install & Preview.
     2535                        if ( $( event.target ).hasClass( 'preview' ) ) {
     2536                                preview = true;
     2537                                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     2538                        }
     2539                },
     2540
     2541                /**
     2542                 * Load theme preview.
     2543                 *
     2544                 * @since 4.9.0
     2545                 *
     2546                 * @param {string} themeId Theme ID.
     2547                 * @returns {jQuery.promise} Promise.
     2548                 */
     2549                loadThemePreview: function( themeId ) {
     2550                        var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
     2551
     2552                        urlParser = document.createElement( 'a' );
     2553                        urlParser.href = location.href;
     2554                        urlParser.search = $.param( _.extend(
     2555                                api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
     2556                                {
     2557                                        theme: themeId,
     2558                                        changeset_uuid: api.settings.changeset.uuid
     2559                                }
     2560                        ) );
     2561
     2562                        // Update loading message. Everything else is handled by reloading the page.
     2563                        $( '#customize-themes-loading-container span' ).hide();
     2564                        $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' );
     2565                        overlay = $( '.wp-full-overlay' );
     2566                        overlay.addClass( 'customize-loading' );
     2567
     2568                        onceProcessingComplete = function() {
     2569                                var request;
     2570                                if ( api.state( 'processing' ).get() > 0 ) {
     2571                                        return;
     2572                                }
     2573
     2574                                api.state( 'processing' ).unbind( onceProcessingComplete );
     2575
     2576                                request = api.requestChangesetUpdate();
     2577                                request.done( function() {
     2578                                        $( window ).off( 'beforeunload.customize-confirm' );
     2579                                        window.location.href = urlParser.href;
     2580                                } );
     2581                                request.fail( function() {
     2582                                        overlay.removeClass( 'customize-loading' );
     2583                                } );
     2584                        };
     2585
     2586                        if ( 0 === api.state( 'processing' ).get() ) {
     2587                                onceProcessingComplete();
     2588                        } else {
     2589                                api.state( 'processing' ).bind( onceProcessingComplete );
     2590                        }
     2591
     2592                        return deferred.promise();
     2593                },
     2594
     2595                /**
     2596                 * Update a theme via wp.updates.
     2597                 *
     2598                 * @since 4.9.0
     2599                 */
     2600                updateTheme: function( event ) {
     2601                        wp.updates.maybeRequestFilesystemCredentials( event );
     2602
     2603                        $( document ).one( 'wp-theme-update-success', function( event, response ) {
     2604                                // Rerender the control to reflect the update.
     2605                                api.control.each( function( control ) {
     2606                                        if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2607                                                control.params.theme.hasUpdate = false;
     2608                                                control.rerenderAsInstalled( true );
     2609                                        }
     2610                                });
     2611                        } );
     2612
     2613                        wp.updates.updateTheme( {
     2614                                slug: $( event.target ).closest( '.notice' ).data( 'slug' )
     2615                        } );
     2616                },
     2617
     2618                /**
     2619                 * Delete a theme via wp.updates.
     2620                 *
     2621                 * @since 4.9.0
     2622                 */
     2623                deleteTheme: function( event ) {
     2624                        var theme, section;
     2625                        theme = $( event.target ).data( 'slug' );
     2626                        section = api.section( 'installed_themes' );
     2627
     2628                        event.preventDefault();
     2629
     2630                        // Confirmation dialog for deleting a theme.
     2631                        if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
     2632                                return;
     2633                        }
     2634
     2635                        wp.updates.maybeRequestFilesystemCredentials( event );
     2636
     2637                        $( document ).one( 'wp-theme-delete-success', function() {
     2638                                var control = api.control( 'installed_theme_' + theme );
     2639
     2640                                // Remove theme control.
     2641                                control.container.remove();
     2642                                api.control.remove( control.id );
     2643
     2644                                // Update installed count.
     2645                                section.loaded = section.loaded - 1;
     2646                                section.updateCount();
     2647
     2648                                // Rerender any other theme controls as uninstalled.
     2649                                api.control.each( function( control ) {
     2650                                        if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
     2651                                                control.rerenderAsInstalled( false );
     2652                                        }
     2653                                });
     2654                        } );
     2655
     2656                        wp.updates.deleteTheme( {
     2657                                slug: theme
     2658                        } );
     2659
     2660                        // Close modal and focus the section.
     2661                        section.closeDetails();
     2662                        section.focus();
     2663                }
     2664
     2665        });
     2666
     2667
    20642668        /**
    20652669         * A Customizer Control.
    20662670         *
     
    24103014                 * @param {Boolean}  active
    24113015                 * @param {Object}   args
    24123016                 * @param {Number}   args.duration
    2413                  * @param {Callback} args.completeCallback
     3017                 * @param {Function} args.completeCallback
    24143018                 */
    24153019                onChangeActive: function ( active, args ) {
    24163020                        if ( args.unchanged ) {
     
    35824186        api.ThemeControl = api.Control.extend({
    35834187
    35844188                touchDrag: false,
    3585                 isRendered: false,
    3586 
    3587                 /**
    3588                  * Defer rendering the theme control until the section is displayed.
    3589                  *
    3590                  * @since 4.2.0
    3591                  */
    3592                 renderContent: function () {
    3593                         var control = this,
    3594                                 renderContentArgs = arguments;
    3595 
    3596                         api.section( control.section(), function( section ) {
    3597                                 if ( section.expanded() ) {
    3598                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
    3599                                         control.isRendered = true;
    3600                                 } else {
    3601                                         section.expanded.bind( function( expanded ) {
    3602                                                 if ( expanded && ! control.isRendered ) {
    3603                                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
    3604                                                         control.isRendered = true;
    3605                                                 }
    3606                                         } );
    3607                                 }
    3608                         } );
    3609                 },
     4189                screenshotRendered: false,
    36104190
    36114191                /**
    36124192                 * @since 4.2.0
     
    36304210                                }
    36314211
    36324212                                // Prevent the modal from showing when the user clicks the action button.
    3633                                 if ( $( event.target ).is( '.theme-actions .button' ) ) {
    3634                                         return;
    3635                                 }
    3636 
    3637                                 api.section( control.section() ).loadThemePreview( control.params.theme.id );
    3638                         });
    3639 
    3640                         control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
    3641                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
     4213                                if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
    36424214                                        return;
    36434215                                }
    36444216
    36454217                                event.preventDefault(); // Keep this AFTER the key filter above
    3646 
    36474218                                api.section( control.section() ).showDetails( control.params.theme );
    36484219                        });
    36494220
     
    36544225                                if ( source ) {
    36554226                                        $screenshot.attr( 'src', source );
    36564227                                }
     4228                                control.screenshotRendered = true;
    36574229                        });
    36584230                },
    36594231
    36604232                /**
    3661                  * Show or hide the theme based on the presence of the term in the title, description, and author.
     4233                 * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
    36624234                 *
    36634235                 * @since 4.2.0
    36644236                 */
     
    36744246                        } else {
    36754247                                control.deactivate();
    36764248                        }
     4249                },
     4250
     4251                /**
     4252                 * Rerender the theme from its JS template with the installed type.
     4253                 *
     4254                 * @since 4.9.0
     4255                 */
     4256                rerenderAsInstalled: function( installed ) {
     4257                        var control = this, section;
     4258                        if ( installed ) {
     4259                                control.params.theme.type = 'installed';
     4260                        } else {
     4261                                section = api.section( control.params.section );
     4262                                control.params.theme.type = section.params.action;
     4263                        }
     4264                        control.renderContent(); // replaces existing content
     4265                        control.container.trigger( 'render-screenshot' );
    36774266                }
    36784267        });
    36794268
     
    46195208                theme:               api.ThemeControl,
    46205209                code_editor:         api.CodeEditorControl
    46215210        };
    4622         api.panelConstructor = {};
     5211        api.panelConstructor = {
     5212                themes: api.ThemesPanel
     5213        };
    46235214        api.sectionConstructor = {
    46245215                themes: api.ThemesSection
    46255216        };
     
    47375328
    47385329                // Sort the sections within each panel
    47395330                api.panel.each( function ( panel ) {
     5331                        if ( 'themes' === panel.id ) {
     5332                                return; // Don't reflow theme sections, as doing so moves them after the themes container.
     5333                        }
     5334
    47405335                        var sections = panel.sections(),
    47415336                                sectionHeadContainers = _.pluck( sections, 'headContainer' );
    47425337                        rootNodes.push( panel );
     
    54596054                        // Collapse the most granular expanded object.
    54606055                        collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
    54616056                        if ( collapsedObject ) {
     6057                                if ( 'themes' === collapsedObject.params.type ) {
     6058                                        // Themes panel or section.
     6059                                        if ( $( 'body' ).hasClass( 'modal-open' ) ) {
     6060                                                collapsedObject.closeDetails();
     6061                                        } else {
     6062                                                // If we're collapsing a section, collapse the panel also.
     6063                                                wp.customize.panel( 'themes' ).collapse();
     6064                                        }
     6065                                        return;
     6066                                }
    54626067                                collapsedObject.collapse();
    54636068                                event.preventDefault();
    54646069                        }
  • src/wp-admin/js/updates.js

    diff --git a/src/wp-admin/js/updates.js b/src/wp-admin/js/updates.js
    index 233714f589..195c64c748 100644
    a b  
    183183                if ( $notice.length ) {
    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
    189193                $document.trigger( 'wp-updates-notice-added' );
     
    930934                if ( 'themes-network' === pagenow ) {
    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' );
    935950
     
    972987                        },
    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' );
    977996
     
    10261045                        return;
    10271046                }
    10281047
     1048                if ( 'customize' === pagenow ) {
     1049                        $theme = wp.customize.control( 'installed_theme_' + response.slug ).container;
     1050                }
     1051
    10291052                if ( 'themes-network' === pagenow ) {
    10301053                        $notice = $theme.find( '.update-message ' );
    10311054                } else {
     
    11621185                        return;
    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
    11731207                $button
  • src/wp-includes/class-wp-customize-manager.php

    diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php
    index 648740df18..a957046c5c 100644
    a b public function __construct( $args = array() ) { 
    288288
    289289                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
    290290
     291                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
    291292                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
    292293                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
    293294                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
    public function __construct( $args = array() ) { 
    343344
    344345                add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
    345346                add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
     347                add_action( 'wp_ajax_customize-load-themes',    array( $this, 'load_themes_ajax' ) );
    346348
    347349                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
    348350                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
    public function __construct( $args = array() ) { 
    359361
    360362                // Export the settings to JS via the _wpCustomizeSettings variable.
    361363                add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
     364
     365                // Add theme update notices.
     366                if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
     367                        require_once( ABSPATH . '/wp-admin/includes/update.php' );
     368                        add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
     369                }
    362370        }
    363371
    364372        /**
    public function enqueue_control_scripts() { 
    33233331                foreach ( $this->controls as $control ) {
    33243332                        $control->enqueue();
    33253333                }
     3334
     3335                if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
     3336                        wp_enqueue_script( 'updates' );
     3337                }
    33263338        }
    33273339
    33283340        /**
    public function get_nonces() { 
    35273539                $nonces = array(
    35283540                        'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
    35293541                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
     3542                        'switch-themes' => wp_create_nonce( 'switch-themes' ),
    35303543                );
    35313544
    35323545                /**
    public function customize_pane_settings() { 
    36003613                        'autofocus' => $this->get_autofocus(),
    36013614                        'documentTitleTmpl' => $this->get_document_title_template(),
    36023615                        'previewableDevices' => $this->get_previewable_devices(),
     3616                        'l10n' => array(
     3617                                'confirmDeleteTheme' => __( 'Are you sure you want to delete this theme?' ),
     3618                                /* translators: %d is the number of theme search results, which cannot currently consider singular vs. plural forms */
     3619                                'themeSearchResults' => __( '%d themes found' ),
     3620                                /* translators: %d is the number of themes being displayed, which cannot currently consider singular vs. plural forms */
     3621                                'announceThemeCount' => __( 'Displaying %d themes' ),
     3622                                'announceThemeDetails' => __( 'Showing details for theme: %s' ),
     3623                        ),
    36033624                );
    36043625
    36053626                // Prepare Customize Section objects to pass to JavaScript.
    public function register_controls() { 
    37023723
    37033724                /* Panel, Section, and Control Types */
    37043725                $this->register_panel_type( 'WP_Customize_Panel' );
     3726                $this->register_panel_type( 'WP_Customize_Themes_Panel' );
    37053727                $this->register_section_type( 'WP_Customize_Section' );
    37063728                $this->register_section_type( 'WP_Customize_Sidebar_Section' );
     3729                $this->register_section_type( 'WP_Customize_Themes_Section' );
    37073730                $this->register_control_type( 'WP_Customize_Color_Control' );
    37083731                $this->register_control_type( 'WP_Customize_Media_Control' );
    37093732                $this->register_control_type( 'WP_Customize_Upload_Control' );
    public function register_controls() { 
    37153738                $this->register_control_type( 'WP_Customize_Theme_Control' );
    37163739                $this->register_control_type( 'WP_Customize_Code_Editor_Control' );
    37173740
    3718                 /* Themes */
     3741                /* Themes (controls are loaded via ajax) */
    37193742
    3720                 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
    3721                         'title'      => $this->theme()->display( 'Name' ),
    3722                         'capability' => 'switch_themes',
    3723                         'priority'   => 0,
     3743                $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array(
     3744                        'title'       => $this->theme()->display( 'Name' ),
     3745                        '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.' ),
     3746                        'capability'  => 'switch_themes',
     3747                        'priority'    => 0,
    37243748                ) ) );
    37253749
    3726                 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
    3727                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
    3728                         'capability' => 'switch_themes',
     3750                $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array(
     3751                        'title'       => __( 'Installed' ),
     3752                        'action'      => 'installed',
     3753                        'capability'  => 'switch_themes',
     3754                        'panel'       => 'themes',
     3755                        'priority'    => 0,
    37293756                ) ) );
    37303757
    3731                 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
    3732 
    3733                 // Theme Controls.
    3734 
    3735                 // Add a control for the active/original theme.
    3736                 if ( ! $this->is_theme_active() ) {
    3737                         $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
    3738                         $active_theme = current( $themes );
    3739                         $active_theme['isActiveTheme'] = true;
    3740                         $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
    3741                                 'theme'    => $active_theme,
    3742                                 'section'  => 'themes',
    3743                                 'settings' => 'active_theme',
     3758                if ( ! is_multisite() ) {
     3759                        $this->add_section( new WP_Customize_Themes_Section( $this, 'wporg_themes', array(
     3760                                'title'       => __( 'WordPress.org' ),
     3761                                'action'      => 'wporg',
     3762                                'capability'  => 'install_themes',
     3763                                'panel'       => 'themes',
     3764                                'priority'    => 5,
    37443765                        ) ) );
    37453766                }
    37463767
    3747                 $themes = wp_prepare_themes_for_js();
    3748                 foreach ( $themes as $theme ) {
    3749                         if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
    3750                                 continue;
    3751                         }
    3752 
    3753                         $theme_id = 'theme_' . $theme['id'];
    3754                         $theme['isActiveTheme'] = false;
    3755                         $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
    3756                                 'theme'    => $theme,
    3757                                 'section'  => 'themes',
    3758                                 'settings' => 'active_theme',
    3759                         ) ) );
    3760                 }
     3768                // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
     3769                $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
     3770                        'capability' => 'switch_themes',
     3771                ) ) );
    37613772
    37623773                /* Site Identity */
    37633774
    public function register_dynamic_settings() { 
    42594270        }
    42604271
    42614272        /**
     4273         * Load themes into the theme browsing/installation UI.
     4274         *
     4275         * @since 4.9.0
     4276         */
     4277        public function load_themes_ajax() {
     4278                check_ajax_referer( 'switch-themes', 'switch-themes-nonce' );
     4279
     4280                if ( ! current_user_can( 'switch_themes' ) ) {
     4281                        wp_die( -1 );
     4282                }
     4283
     4284                if ( empty( $_POST['theme_action'] ) ) {
     4285                        wp_send_json_error( 'missing_theme_action' );
     4286                }
     4287
     4288                require_once( ABSPATH . 'wp-admin/includes/theme.php' );
     4289                if ( 'installed' === $_POST['theme_action'] ) {
     4290                        $themes = array( 'themes' => wp_prepare_themes_for_js() );
     4291                        foreach ( $themes['themes'] as &$theme ) {
     4292                                $theme['type'] = 'installed';
     4293                                // Set active based on customized theme.
     4294                                if ( $_POST['customized_theme'] === $theme['id'] ) {
     4295                                        $theme['active'] = true;
     4296                                } else {
     4297                                        $theme['active'] = false;
     4298                                }
     4299                        }
     4300                } elseif ( 'wporg' === $_POST['theme_action'] ) {
     4301                        if ( ! current_user_can( 'install_themes' ) ) {
     4302                                wp_die( -1 );
     4303                        }
     4304
     4305                        // Arguments for all queries.
     4306                        $args = array(
     4307                                'per_page' => 100,
     4308                                'page' => absint( $_POST['page'] ),
     4309                                'fields' => array(
     4310                                        'screenshot_url' => true,
     4311                                        'description' => true,
     4312                                        'rating' => true,
     4313                                        'downloaded' => true,
     4314                                        'downloadlink' => true,
     4315                                        'last_updated' => true,
     4316                                        'homepage' => true,
     4317                                        'num_ratings' => true,
     4318                                        'tags' => true,
     4319                                        'parent' => true,
     4320                                        //'extended_author' => true, @todo: WordPress.org throws a 500 server error when this is here.
     4321                                ),
     4322                        );
     4323
     4324                        // Define query filters based on user input.
     4325                        if ( ! array_key_exists( 'search', $_POST ) ) {
     4326                                $args['search'] = '';
     4327                        } else {
     4328                                $args['search'] = wp_unslash( $_POST['search'] );
     4329                        }
     4330
     4331                        if ( ! array_key_exists( 'tags', $_POST ) ) {
     4332                                $args['tag'] = '';
     4333                        } else {
     4334                                $args['tag'] = wp_unslash( $_POST['tags'] );
     4335                        }
     4336
     4337                        if ( '' === $args['search'] && '' === $args['tag'] ) {
     4338                                $args['browse'] = 'new'; // Sort by latest themes by default.
     4339                        }
     4340
     4341                        // Load themes from the .org API.
     4342                        $themes = themes_api( 'query_themes', $args );
     4343                        if ( is_wp_error( $themes ) ) {
     4344                                wp_send_json_error();
     4345                        }
     4346
     4347                        // This list matches the allowed tags in wp-admin/includes/theme-install.php.
     4348                        $themes_allowedtags = array('a' => array('href' => array(), 'title' => array(), 'target' => array()),
     4349                                'abbr' => array('title' => array()), 'acronym' => array('title' => array()),
     4350                                'code' => array(), 'pre' => array(), 'em' => array(), 'strong' => array(),
     4351                                'div' => array(), 'p' => array(), 'ul' => array(), 'ol' => array(), 'li' => array(),
     4352                                'h1' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'h5' => array(), 'h6' => array(),
     4353                                'img' => array('src' => array(), 'class' => array(), 'alt' => array())
     4354                        );
     4355
     4356                        // Prepare a list of installed themes to check against before the loop.
     4357                        $installed_themes = array();
     4358                        $wp_themes = wp_get_themes();
     4359                        foreach ( $wp_themes as $theme ) {
     4360                                $installed_themes[] = $theme->get_stylesheet();
     4361                        }
     4362                        $update_php = network_admin_url( 'update.php?action=install-theme' );
     4363
     4364                        // Set up properties for themes available on WordPress.org.
     4365                        foreach ( $themes->themes as &$theme ) {
     4366                                $theme->install_url = add_query_arg( array(
     4367                                        'theme'    => $theme->slug,
     4368                                        '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
     4369                                ), $update_php );
     4370
     4371                                $theme->name        = wp_kses( $theme->name, $themes_allowedtags );
     4372                                $theme->author      = wp_kses( $theme->author, $themes_allowedtags );
     4373                                $theme->version     = wp_kses( $theme->version, $themes_allowedtags );
     4374                                $theme->description = wp_kses( $theme->description, $themes_allowedtags );
     4375                                $theme->tags        = implode( ', ', $theme->tags );
     4376                                $theme->stars       = wp_star_rating( array( 'rating' => $theme->rating, 'type' => 'percent', 'number' => $theme->num_ratings, 'echo' => false ) );
     4377                                $theme->num_ratings = number_format_i18n( $theme->num_ratings );
     4378                                $theme->preview_url = set_url_scheme( $theme->preview_url );
     4379
     4380                                // Handle themes that are already installed as installed themes.
     4381                                if ( in_array( $theme->slug, $installed_themes, true ) ) {
     4382                                        $theme->type = 'installed';
     4383                                } else {
     4384                                        $theme->type = $_POST['theme_action'];
     4385                                }
     4386
     4387                                // Set active based on customized theme.
     4388                                if ( $_POST['customized_theme'] === $theme->slug ) {
     4389                                        $theme->active = true;
     4390                                } else {
     4391                                        $theme->active = false;
     4392                                }
     4393
     4394                                // Map available theme properties to installed theme properties.
     4395                                $theme->id           = $theme->slug;
     4396                                $theme->screenshot   = array( $theme->screenshot_url );
     4397                                $theme->authorAndUri = $theme->author;
     4398                                $theme->parent       = ( $theme->slug === $theme->template ) ? false: $theme->template; // The .org API does not seem to return the parent in a documneted way; however, this check should yield a similar result in most cases.
     4399                                unset( $theme->slug );
     4400                                unset( $theme->screenshot_url );
     4401                                unset( $theme->author );
     4402                        } // End foreach().
     4403                } // End if().
     4404                wp_send_json_success( $themes );
     4405        }
     4406
     4407
     4408        /**
    42624409         * Callback for validating the header_textcolor value.
    42634410         *
    42644411         * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
  • src/wp-includes/customize/class-wp-customize-theme-control.php

    diff --git a/src/wp-includes/customize/class-wp-customize-theme-control.php b/src/wp-includes/customize/class-wp-customize-theme-control.php
    index ffc9c878ae..556ce22698 100644
    a b public function render_content() {} 
    5757         * @since 4.2.0
    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="" />
    7478                                </div>
    public function content_template() { 
    7680                                <div class="theme-screenshot blank"></div>
    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
    8686                                /* translators: Theme author name */
    8787                                printf( _x( 'By %s', 'theme author' ), '{{ data.theme.author }}' );
    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 }}"><p><?php printf( __( 'New version available. %s' ), '<button class="button-link update-theme" type="button">' . __( 'Update now' ) . '</button>' ); ?></p></div>
     92                        <# } #>
     93
     94                        <# if ( data.theme.active ) { #>
     95                                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">
    9296                                        <?php
    9397                                        /* translators: %s: theme name */
    94                                         printf( __( '<span>Active:</span> %s' ), '{{{ data.theme.name }}}' );
     98                                        printf( __( '<span>Current:</span> %s' ), '{{ data.theme.name }}' );
    9599                                        ?>
    96100                                </h3>
     101                                <div class="theme-actions">
     102                                        <button type="button" class="button button-primary customize-theme" aria-label="<?php echo esc_attr( $customize_label ); ?>"><?php _e( 'Customize' ); ?></button>
     103                                </div>
     104                                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
     105                        <# } else if ( 'installed' === data.theme.type ) { #>
     106                                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
     107                                <div class="theme-actions">
     108                                        <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>
     109                                </div>
     110                                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
    97111                        <# } else { #>
    98                                 <h3 class="theme-name" id="{{ data.theme.id }}-name">{{{ data.theme.name }}}</h3>
     112                                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
    99113                                <div class="theme-actions">
    100                                         <button type="button" class="button theme-details"><?php _e( 'Theme Details' ); ?></button>
     114                                        <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 & Preview' ); ?></button>
    101115                                </div>
    102116                        <# } #>
    103117                </div>
  • new file src/wp-includes/customize/class-wp-customize-themes-panel.php

    diff --git a/src/wp-includes/customize/class-wp-customize-themes-panel.php b/src/wp-includes/customize/class-wp-customize-themes-panel.php
    new file mode 100644
    index 0000000000..c170b34a94
    - +  
     1<?php
     2/**
     3 * Customize API: WP_Customize_Themes_Panel class
     4 *
     5 * @package WordPress
     6 * @subpackage Customize
     7 * @since 4.9.0
     8 */
     9
     10/**
     11 * Customize Themes Panel Class
     12 *
     13 * @since 4.9.0
     14 *
     15 * @see WP_Customize_Panel
     16 */
     17class WP_Customize_Themes_Panel extends WP_Customize_Panel {
     18
     19        /**
     20         * Panel type.
     21         *
     22         * @since 4.9.0
     23         * @var string
     24         */
     25        public $type = 'themes';
     26
     27        /**
     28         * An Underscore (JS) template for rendering this panel's container.
     29         *
     30         * The themes panel renders a custom panel heading with the current theme and a switch themes button.
     31         *
     32         * @see WP_Customize_Panel::print_template()
     33         *
     34         * @since 4.9.0
     35         */
     36        protected function render_template() {
     37                ?>
     38                <li id="accordion-section-{{ data.id }}" class="accordion-section control-panel-themes">
     39                        <h3 class="accordion-section-title">
     40                                <?php
     41                                if ( $this->manager->is_theme_active() ) {
     42                                        echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> {{ data.title }}';
     43                                } else {
     44                                        echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> {{ data.title }}';
     45                                }
     46                                ?>
     47
     48                                <?php
     49                                if ( current_user_can( 'switch_themes' ) ) : ?>
     50                                        <button type="button" class="button change-theme" aria-label="<?php _e( 'Change theme' ); ?>"><?php _ex( 'Change', 'theme' ); ?></button>
     51                                <?php endif; ?>
     52                        </h3>
     53                        <ul class="accordion-sub-container control-panel-content"></ul>
     54                </li>
     55                <?php
     56        }
     57
     58        /**
     59         * An Underscore (JS) template for this panel's content (but not its container).
     60         *
     61         * Class variables for this panel class are available in the `data` JS object;
     62         * export custom variables by overriding WP_Customize_Panel::json().
     63         *
     64         * @since 4.9.0
     65         *
     66         * @see WP_Customize_Panel::print_template()
     67         */
     68        protected function content_template() {
     69                ?>
     70                <li class="panel-meta customize-info accordion-section <# if ( ! data.description ) { #> cannot-expand<# } #>">
     71                        <button class="customize-panel-back" tabindex="-1"><span class="screen-reader-text"><?php _e( 'Back' ); ?></span></button>
     72                        <div class="accordion-section-title">
     73                                <span class="preview-notice"><?php
     74                                        /* translators: %s: themes panel title in the Customizer */
     75                                        echo sprintf( __( 'You are browsing %s' ), '<strong class="panel-title">' . __( 'Themes' ) . '</strong>' ); // Separate strings for consistency with other panels.
     76                                ?></span>
     77                                <?php if ( current_user_can( 'install_themes' ) && ! is_multisite() ) : ?>
     78                                        <# if ( data.description ) { #>
     79                                                <button class="customize-help-toggle dashicons dashicons-editor-help" tabindex="0" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
     80                                        <# } #>
     81                                <?php endif; ?>
     82                        </div>
     83                        <?php if ( current_user_can( 'install_themes' ) && ! is_multisite() ) : ?>
     84                                <# if ( data.description ) { #>
     85                                        <div class="description customize-panel-description">
     86                                                {{{ data.description }}}
     87                                        </div>
     88                                <# } #>
     89                        <?php endif; ?>
     90                </li>
     91                <li id="customize-themes-loading-container">
     92                        <span class="customize-loading-text-installing-theme"><?php _e( 'Downloading your new theme&hellip;' ); ?></span>
     93                        <span class="customize-loading-text"><?php _e( 'Setting up your live preview. This may take a bit.' ); ?></span>
     94                </li><?php // Used as a full-screen overlay transition after clicking to preview a theme. ?>
     95                <li class="customize-themes-full-container-container">
     96                        <ul class="customize-themes-full-container">
     97                                <li class="customize-themes-notifications"></li>
     98                        </ul>
     99                </li>
     100                <?php
     101        }
     102}
  • src/wp-includes/customize/class-wp-customize-themes-section.php

    diff --git a/src/wp-includes/customize/class-wp-customize-themes-section.php b/src/wp-includes/customize/class-wp-customize-themes-section.php
    index 415ef47222..6573d1e759 100644
    a b  
    1010/**
    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
    1616 *
    1717 * @see WP_Customize_Section
    1818 */
    1919class WP_Customize_Themes_Section extends WP_Customize_Section {
    20 
    2120        /**
    22          * Customize section type.
     21         * Section type.
    2322         *
    2423         * @since 4.2.0
    2524         * @var string
    class WP_Customize_Themes_Section extends WP_Customize_Section { 
    2726        public $type = 'themes';
    2827
    2928        /**
    30          * Render the themes section, which behaves like a panel.
     29         * Theme section action.
    3130         *
    32          * @since 4.2.0
     31         * Defines the type of themes to load (installed, wporg, etc.).
     32         *
     33         * @since 4.9.0
     34         * @var string
    3335         */
    34         protected function render() {
    35                 $classes = 'accordion-section control-section control-section-' . $this->type;
    36                 ?>
    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                                 ?>
     36        public $action = '';
    4637
    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>
     38        /**
     39         * Get section parameters for JS.
     40         *
     41         * @since 4.9.0
     42         * @return array Exported parameters.
     43         */
     44        public function json() {
     45                $exported = parent::json();
     46                $exported['action'] = $this->action;
    6847
    69                                 <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
     48                return $exported;
     49        }
    7050
    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; ?>
     51        /**
     52         * Render a themes section as a JS template.
     53         *
     54         * The template is only rendered by PHP once, so all actions are prepared at once on the server side.
     55         *
     56         * @since 4.9.0
     57         */
     58        protected function render_template() {
     59                ?>
     60                <li id="accordion-section-{{ data.id }}" class="theme-section">
     61                        <button type="button" class="customize-themes-section-title themes-section-{{ data.id }}">{{ data.title }}</button>
     62                        <?php if ( current_user_can( 'install_themes' ) || is_multisite() ) : // @todo: upload support ?>
     63                        <?php endif; ?>
     64                        <div class="customize-themes-section themes-section-{{ data.id }} control-section-content themes-php">
     65                                <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
    7866                                <div class="theme-browser rendered">
    79                                         <ul class="themes accordion-section-content">
    80                                         </ul>
     67                                        <div class="customize-preview-header themes-filter-bar">
     68                                                <?php $this->filter_bar_content_template(); ?>
     69                                        </div>
     70                                        <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>
     71                                        <ul class="themes">
     72                                        </ul>
     73                                        <p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p>
     74                                        <p class="spinner"></p>
    8175                                </div>
    8276                        </div>
    8377                </li>
    8478<?php }
     79
     80        /**
     81         * Render the filter bar portion of a themes section as a JS template.
     82         *
     83         * The template is only rendered by PHP once, so all actions are prepared at once on the server side.
     84         * The filter bar container is rendered by @see `render_template()`.
     85         *
     86         * @since 4.9.0
     87         */
     88        protected function filter_bar_content_template() {
     89                ?>
     90                <# if ( 'wporg' === data.action ) { #>
     91                        <div class="search-form">
     92                                <label class="screen-reader-text" for="wp-filter-search-input"><?php _e( 'Search themes&hellip;' ); ?></label>
     93                                <input placeholder="<?php _e( 'Search themes&hellip;' ); ?>" type="text" aria-describedby="live-search-desc" id="wp-filter-search-input" class="wp-filter-search">
     94                                <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span>
     95                        </div>
     96                        <button type="button" class="button feature-filter-toggle">
     97                                <?php /* translators: %s: number of filters selected. */
     98                                printf( __( 'Filter themes (%s)' ), '<span class="theme-filter-count">0</span>' ); ?>
     99                                </button>
     100                        <div class="filter-drawer filter-details">
     101                                <?php
     102                                $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.
     103                                foreach ( $feature_list as $feature_name => $features ) {
     104                                        echo '<fieldset class="filter-group">';
     105                                        $feature_name = esc_html( $feature_name );
     106                                        echo '<legend>' . $feature_name . '</legend>';
     107                                        echo '<div class="filter-group-feature">';
     108                                        foreach ( $features as $feature => $feature_name ) {
     109                                                $feature = esc_attr( $feature );
     110                                                echo '<input type="checkbox" id="filter-id-' . $feature . '" value="' . $feature . '" /> ';
     111                                                echo '<label for="filter-id-' . $feature . '">' . $feature_name . '</label><br>';
     112                                        }
     113                                        echo '</div>';
     114                                        echo '</fieldset>';
     115                                }
     116                                ?>
     117                        </div>
     118                <# } else { #>
     119                        <p class="themes-filter-container">
     120                                <label for="themes-filter">
     121                                        <span class="screen-reader-text"><?php _e( 'Search themes&hellip;' ); ?></span>
     122                                        <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search themes&hellip;' ); ?>" aria-describedby="live-search-desc" class="wp-filter-search wp-filter-search-themes" />
     123                                        <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span>
     124                                </label>
     125                        </p>
     126                <# } #>
     127                <div class="filter-themes-count">
     128                        <span class="themes-displayed"><?php
     129                                /* translators: %s: number of themes displayed. */
     130                                echo sprintf( __( '%s themes' ), '<span class="theme-count">0</span>' );
     131                        ?></span>
     132                        <button type="button" class="button button-primary filter-themes"><?php _e( 'Filter themes' ); ?></button>
     133                </div>
     134        <?php }
    85135}
  • tests/phpunit/tests/customize/manager.php

    diff --git a/tests/phpunit/tests/customize/manager.php b/tests/phpunit/tests/customize/manager.php
    index 1955a1cd74..0af267cbd5 100644
    a b function test_customize_pane_settings() { 
    23512351                $data = json_decode( $json, true );
    23522352                $this->assertNotEmpty( $data );
    23532353
    2354                 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts' ), array_keys( $data ) );
     2354                $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts', 'l10n' ), array_keys( $data ) );
    23552355                $this->assertEquals( $autofocus, $data['autofocus'] );
    23562356                $this->assertArrayHasKey( 'save', $data['nonce'] );
    23572357                $this->assertArrayHasKey( 'preview', $data['nonce'] );