WordPress.org

Make WordPress Core

Ticket #37661: 37661.10.1.diff

File 37661.10.1.diff, 101.0 KB (added by celloexpressions, 21 months ago)

Fix patch format (hopefully).

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

     
    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;
     
    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;
     
    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,
     
    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;
     
    12381220        100% { opacity: 1; }
    12391221}
    12401222
    1241 /* #customize-container is reused from customize-loader.js, hence the naming. */
    1242 .wp-customizer .customize-loading #customize-container {
     1223.wp-customizer .customize-loading #customize-themes-loading-container {
    12431224        display: block;
    12441225        -webkit-animation: customize-reload .75s; /* Can't use `transition` because `display` changes here. */
    12451226        animation: customize-reload .75s;
    12461227}
    12471228
    1248 #customize-theme-controls .control-section-themes .accordion-section-title:hover, /* Not a focusable element. */
    1249 #customize-theme-controls .control-section-themes .accordion-section-title {
     1229.customize-loading #customize-themes-loading-container span {
     1230    clear: both;
     1231    color: #555d66;
     1232    font-size: 18px;
     1233    font-style: normal;
     1234    margin: 0;
     1235    padding: 100px 0;
     1236    text-align: center;
     1237        width: 100%;
     1238        display: block;
     1239}
     1240
     1241.customize-loading #customize-themes-loading-container .customize-loading-text {
     1242        display: none;
     1243}
     1244
     1245#customize-theme-controls .control-panel-themes {
     1246        border-bottom: none;
     1247}
     1248
     1249#customize-theme-controls .control-panel-themes > .accordion-section-title:hover, /* Not a focusable element. */
     1250#customize-theme-controls .control-panel-themes > .accordion-section-title {
    12501251        cursor: default;
    12511252        background: #fff;
    12521253        color: #555d66;
     
    12531254        border-top: 1px solid #ddd;
    12541255        border-bottom: 1px solid #ddd;
    12551256        border-left: none;
    1256         margin-top: 0;
     1257        border-right: none;
     1258        margin: 0 0 15px 0;
     1259        padding-right: 100px; /* Space for the button */
    12571260}
    1258 #customize-theme-controls .control-section-themes .customize-section-back {
    1259         position: absolute;
    1260         right: 0;
    1261         top: 0;
    1262         height: 80px;
    1263         border-left: 1px solid #ddd;
    1264         border-right: 4px solid #fff;
    1265 }
    1266 #customize-theme-controls .control-section-themes .customize-section-back:before {
    1267         content: "\f345";
    1268 }
    1269 #customize-theme-controls .control-section-themes .customize-section-back:hover,
    1270 #customize-theme-controls .control-section-themes .customize-section-back:focus {
    1271         border-right-color: #0073aa;
    1272 }
    12731261
    12741262#customize-theme-controls .control-section-themes .customize-themes-panel .accordion-section-title:first-child:hover, /* Not a focusable element. */
    12751263#customize-theme-controls .control-section-themes .customize-themes-panel .accordion-section-title:first-child {
     
    12761264        border-top: 0;
    12771265}
    12781266
    1279 #customize-theme-controls .control-section-themes > .accordion-section-title:hover, /* Not a focusable element. */
    1280 #customize-theme-controls .control-section-themes > .accordion-section-title {
    1281         margin: 0 0 15px;
    1282 }
    1283 
    1284 #customize-controls .customize-themes-panel .accordion-section-title:hover,
    1285 #customize-controls .customize-themes-panel .accordion-section-title {
    1286         margin: 15px -8px;
    1287 }
    1288 
    1289 #customize-controls .control-section-themes .accordion-section-title,
    1290 #customize-controls .customize-themes-panel .accordion-section-title {
    1291         padding-right: 100px; /* Space for the button */
    1292 }
    1293 
    1294 #customize-controls .control-section-themes .accordion-section-title span.customize-action,
     1267.control-panel-themes .accordion-section-title span.customize-action,
    12951268#customize-controls .customize-section-title span.customize-action {
    12961269        font-size: 13px;
    12971270        display: block;
     
    12981271        font-weight: 400;
    12991272}
    13001273
    1301 #customize-controls .control-section-themes .accordion-section-title .change-theme,
    1302 #customize-controls .customize-themes-panel .accordion-section-title .customize-theme {
     1274.control-panel-themes .accordion-section-title .change-theme {
    13031275        position: absolute;
    13041276        right: 10px;
    13051277        top: 50%;
     
    13071279        font-weight: 400;
    13081280}
    13091281
    1310 #customize-controls .control-section-themes .accordion-section-title:before {
     1282#customize-theme-controls .control-panel-themes > .accordion-section-title:after {
    13111283        display: none;
    13121284}
    13131285
    1314 #customize-controls .customize-themes-panel {
    1315         padding: 0 8px;
    1316         background: #f1f1f1;
    1317         box-sizing: border-box;
     1286.control-panel-themes .customize-themes-full-container {
     1287        position: fixed;
     1288        top: 0;
     1289        left: 0;
     1290        -webkit-transition: .18s left ease-in-out;
     1291        transition: .18s left ease-in-out;
     1292        margin: 0 0 0 300px;
     1293        padding:25px;
     1294        overflow-y: scroll;
     1295        width: -webkit-calc(100% - 350px);
     1296        width: calc(100% - 350px);
     1297        height: -webkit-calc(100% - 50px);
     1298        height: calc(100% - 50px);
     1299        background: #eee;
     1300        z-index: 20;
    13181301}
    13191302
    1320 #customize-controls .customize-themes-panel .accordion-section-title:first-child {
    1321         margin-top: 0;
     1303/* Animations for opening the themes panel */
     1304#customize-header-actions .save,
     1305#customize-header-actions .spinner,
     1306#customize-header-actions .customize-controls-preview-toggle {
     1307        position: relative;
     1308        top: 0;
     1309        -webkit-transition: .18s top ease-in-out;
     1310        transition: .18s top ease-in-out;
    13221311}
    13231312
    1324 #customize-controls .customize-themes-panel .accordion-section-title:nth-child(2) {
     1313#customize-footer-actions,
     1314#customize-footer-actions .collapse-sidebar {
     1315        bottom: 0;
     1316        -webkit-transition: .18s bottom ease-in-out;
     1317        transition: .18s bottom ease-in-out;
     1318}
     1319
     1320.in-themes-panel:not(.animating) #customize-header-actions .save,
     1321.in-themes-panel:not(.animating) #customize-header-actions .spinner,
     1322.in-themes-panel:not(.animating) #customize-header-actions .customize-controls-preview-toggle,
     1323.in-themes-panel:not(.animating) #customize-preview,
     1324.in-themes-panel:not(.animating) #customize-footer-actions {
     1325        visibility: hidden;
     1326}
     1327
     1328.wp-full-overlay.in-themes-panel {
     1329        background: #eee; /* Prevents a black flash when fading in the panel */
     1330}
     1331
     1332.in-themes-panel #customize-header-actions .save,
     1333.in-themes-panel #customize-header-actions .spinner,
     1334.in-themes-panel #customize-header-actions .customize-controls-preview-toggle {
     1335        top: -45px;
     1336}
     1337
     1338.in-themes-panel #customize-footer-actions,
     1339.in-themes-panel #customize-footer-actions .collapse-sidebar {
     1340        bottom: -45px;
     1341}
     1342
     1343/* Don't show the theme count while the panel opens, as it's in the wrong place during the animation */
     1344.in-themes-panel.animating .control-panel-themes .filter-themes-count {
     1345        display: none;
     1346}
     1347
     1348.in-themes-panel.wp-full-overlay .wp-full-overlay-sidebar-content {
     1349        bottom: 0;
     1350}
     1351
     1352/* Adds a delay before fading in to avoid it "jumping" */
     1353@-webkit-keyframes themes-fade-in {
     1354        0% {
     1355                opacity: 0;
     1356        }
     1357        50% {
     1358                opacity: 0;
     1359        }
     1360        100% {
     1361                opacity: 1;
     1362        }
     1363}
     1364@keyframes themes-fade-in {
     1365        0% {
     1366                opacity: 0;
     1367        }
     1368        50% {
     1369                opacity: 0;
     1370        }
     1371        100% {
     1372                opacity: 1;
     1373        }
     1374}
     1375
     1376.control-panel-themes .customize-themes-full-container.animate {
     1377        -webkit-animation: .6s themes-fade-in 1;
     1378        animation: .6s themes-fade-in 1;
     1379}
     1380
     1381.in-themes-panel:not(.animating) .control-panel-themes .filter-themes-count {
     1382        -webkit-animation: .6s themes-fade-in 1;
     1383        animation: .6s themes-fade-in 1;
     1384}
     1385
     1386.control-panel-themes .filter-themes-count {
     1387        position: fixed;
     1388        top: 0;
     1389        left: 48px;
     1390        width: 222px;
     1391        padding: 6px 15px;
     1392        margin: 0;
     1393        line-height: 32px;
     1394        text-align: right;
     1395        z-index: 10;
     1396}
     1397
     1398.control-panel-themes .filter-themes-count .themes-displayed {
     1399        font-weight: 600;
     1400        color: #555d66;
     1401}
     1402
     1403.control-panel-themes .filter-themes-count .see-themes,
     1404.control-panel-themes .filter-themes-count .filter-themes {
     1405        display: none;
     1406}
     1407
     1408
     1409/* Mobile - toggle between themes and filters */
     1410@media screen and (max-width:600px) {
     1411
     1412        /* Show a spinner in the filters view also, reusing the main customize spinner */
     1413        .in-themes-panel.loading #customize-header-actions .spinner {
     1414                position: fixed;
     1415                top: 0;
     1416                left: 48px;
     1417                visibility: visible;
     1418        }
     1419
     1420        .in-themes-panel.loading.showing-themes #customize-header-actions .spinner {
     1421                visibility: hidden;
     1422        }
     1423
     1424        .control-panel-themes .filter-themes-count {
     1425                width: -webkit-calc(100% - 93px);
     1426                width: calc(100% - 93px);
     1427        }
     1428
     1429        .control-panel-themes .filter-themes-count .themes-displayed {
     1430                display: none;
     1431        }
     1432
     1433        .wp-full-overlay:not(.showing-themes) .control-panel-themes .filter-themes-count .see-themes {
     1434                display: block;
     1435                float: right;
     1436        }
     1437
     1438        .wp-full-overlay.showing-themes .control-panel-themes .filter-themes-count .filter-themes {
     1439                display: block;
     1440                float: right;
     1441        }
     1442
     1443        .in-themes-panel.showing-themes .control-panel-themes .customize-panel-back {
     1444                position: fixed;
     1445                top: 0;
     1446                left: 0;
     1447                z-index: 10;
     1448                height: 45px;
     1449                background: #eee;
     1450        }
     1451
     1452        .in-themes-panel.showing-themes .control-panel-themes .customize-panel-back:before {
     1453                line-height: 45px;
     1454        }
     1455
     1456        .control-panel-themes .customize-themes-full-container {
     1457                width: -webkit-calc(100% - 50px);
     1458                width: calc(100% - 50px);
     1459                margin: 0;
     1460                top: 46px;
     1461                height: -webkit-calc(100% - 96px);
     1462                height: calc(100% - 96px);
     1463                z-index: 1;
     1464                display: none;
     1465        }
     1466
     1467        .showing-themes .control-panel-themes .customize-themes-full-container {
     1468                display: block;
     1469        }
     1470}
     1471
     1472.control-panel-themes .customize-themes-notifications .notice {
     1473        margin: 0 0 25px 0;
     1474}
     1475
     1476.customize-themes-full-container .customize-themes-section {
     1477        display: none !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1478        overflow: hidden;
     1479}
     1480
     1481.customize-themes-full-container .customize-themes-section.current-section {
     1482        display: list-item !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1483}
     1484
     1485.theme-section .customize-themes-text-before {
     1486        padding: 0 0 8px 15px;
     1487        margin: 15px 0 0 0;
     1488        line-height: 16px;
     1489        border-bottom: 1px solid #ddd;
     1490        color: #555d66;
     1491}
     1492
     1493.control-panel-themes .customize-themes-section-title {
     1494        width: 100%;
     1495        background: #fff;
     1496        -webkit-box-shadow: none;
     1497        box-shadow: none;
     1498        outline: none;
     1499        border-top: none;
     1500        border-bottom: 1px solid #ddd;
     1501        border-left: 4px solid #fff;
     1502        border-right: none;
     1503        cursor: pointer;
     1504        padding: 10px 15px;
     1505        position: relative;
     1506        text-align: left;
    13251507        font-size: 14px;
    13261508        font-weight: 600;
     1509        color: #555d66;
     1510        text-shadow: none;
    13271511}
    13281512
    1329 #customize-controls .customize-themes-panel > h2 {
    1330         padding: 15px 8px 0 8px;
     1513.control-panel-themes .theme-section {
     1514        margin: 0;
     1515        position: relative;
    13311516}
    13321517
    1333 #customize-theme-controls .customize-themes-panel .accordion-section-content {
     1518.control-panel-themes .customize-themes-section-title:focus,
     1519.control-panel-themes .customize-themes-section-title:hover {
     1520        border-left-color: #0073aa;
     1521        color: #0073aa;
     1522        background: #f5f5f5;
     1523}
     1524
     1525.control-panel-themes .theme-section .customize-themes-section-title.selected:after {
     1526        content: "\f147";
     1527        font: 16px/1 dashicons;
     1528        box-sizing: border-box;
     1529        width: 20px;
     1530        height: 20px;
     1531        padding: 3px 3px 1px 1px; /* Re-align the icon to the smaller grid */
     1532        -webkit-border-radius: 100%;
     1533        border-radius: 100%;
     1534        position: absolute;
     1535        top: 9px;
     1536        right: 15px;
     1537        background: #0073aa;
     1538        color: #fff;
     1539}
     1540
     1541.control-panel-themes .customize-themes-section-title.selected {
     1542        color: #0073aa;
     1543}
     1544
     1545.control-panel-themes .customize-themes-section-title.themes-section-search_themes {
     1546        border-left: none;
     1547        padding: 5px 10px 5px 15px;
     1548        width: auto;
     1549}
     1550
     1551.control-panel-themes .customize-themes-section-title.themes-section-feature_filter_themes:after,
     1552.control-panel-themes .customize-themes-section-title.themes-section-favorites_themes:after {
     1553        content: "\f140";
     1554        font: 20px/1 dashicons;
     1555        position: absolute;
     1556        right: 15px;
     1557        top: 8px;
     1558}
     1559
     1560.control-panel-themes .customize-themes-section-title.themes-section-search_themes .wp-filter-search {
     1561        width: 100%;
     1562}
     1563
     1564.control-panel-themes .customize-themes-section-title.themes-section-search_themes.selected,
     1565.control-panel-themes .customize-themes-section-title.themes-section-search_themes:hover {
     1566        background: #fff;
     1567        cursor: default;
     1568}
     1569
     1570.control-panel-themes .customize-themes-section-title.themes-section-feature_filter_themes {
     1571        margin-top: 15px;
     1572        border-top: 1px solid #ddd;
     1573}
     1574
     1575.control-panel-themes .filter-details {
     1576        background: #f5f5f5;
     1577        margin: 0;
     1578        padding: 8px 15px;
     1579        border-top: none;
     1580        border-bottom: 1px solid #ddd;
     1581        display: none;
     1582}
     1583
     1584.control-panel-themes .customize-themes-section-title.selected.details-open {
     1585        border-bottom-color: #f5f5f5;
     1586    border-left-color: #f5f5f5;
     1587        background: #f5f5f5;
     1588}
     1589
     1590.control-panel-themes .favorites-form.filter-details label {
     1591        padding-bottom: 6px;
     1592        display: inline-block;
     1593}
     1594
     1595.control-panel-themes .filter-details .filter-group {
     1596        float: none;
     1597        width: 100%;
    13341598        background: transparent;
    1335         display: block;
     1599        border: none;
     1600        padding: 0;
     1601        -webkit-box-shadow: none;
     1602        box-shadow: none;
    13361603}
    13371604
    1338 .customize-control.customize-control-theme {
    1339         margin-bottom: 8px;
     1605.control-panel-themes .filter-details .filter-group legend button {
     1606        padding: 18px 15px 8px 10px;
     1607        line-height: 14px;
     1608        border-bottom: 1px solid #ddd;
     1609        width: 100%;
     1610        text-align: left;
     1611        text-decoration: none;
    13401612}
    13411613
     1614.control-panel-themes .filter-details .filter-group legend {
     1615        position: relative;
     1616        top: 0;
     1617        width: 100%;
     1618}
     1619
     1620.control-panel-themes .filter-details .filter-group legend button:after {
     1621        content: "\f140";
     1622        font: 20px/1 dashicons;
     1623        position: absolute;
     1624        bottom: 6px;
     1625        right: 5px;
     1626}
     1627
     1628.control-panel-themes .filter-details .filter-group legend button:hover,
     1629.control-panel-themes .filter-details .filter-group legend button:focus {
     1630        color: #0073aa;
     1631        border-bottom-color: #0073aa; /* Color change for focus style should be acceptable because border-bottom is barely visible previously. */
     1632        outline: none;
     1633        -webkit-box-shadow: none;
     1634        box-shadow: none;
     1635}
     1636
     1637.control-panel-themes .filter-details .filter-group legend button.open:after {
     1638        content: "\f142";
     1639}
     1640
     1641.control-panel-themes .filter-details .filter-group .filter-group-feature {
     1642        display: none;
     1643        margin: 0;
     1644}
     1645
     1646.control-panel-themes .filter-details .filter-group-feature label {
     1647        border: 1px solid #ddd;
     1648        border-top: 0;
     1649        background: #fff;
     1650        color: #555d66;
     1651        margin: 0;
     1652        padding: 12px 10px 12px 34px;
     1653        width: -webkit-calc(100% - 46px);
     1654        width: calc(100% - 46px);
     1655        line-height: 16px;
     1656        font-weight: 600;
     1657}
     1658
     1659.control-panel-themes .filter-details .filter-group-feature input {
     1660        position: absolute;
     1661        margin: 12px 10px;
     1662}
     1663
     1664.control-panel-themes .filter-details .filter-group-feature label:hover {
     1665        color: #0073aa;
     1666}
     1667
    13421668#customize-theme-controls .themes.accordion-section-content {
    13431669        position: relative;
    13441670        left: 0;
     
    13461672        width: 100%;
    13471673}
    13481674
    1349 .wp-customizer .theme-browser .themes {
    1350         padding-bottom: 8px;
     1675.loading .customize-themes-section .spinner {
     1676        display: block;
     1677        visibility: visible;
     1678        position: relative;
     1679        clear: both;
     1680        width: 20px;
     1681        height: 20px;
     1682        left: -webkit-calc(50% - 10px);
     1683        left: calc(50% - 10px);
     1684        float: none;
     1685        margin-top: 50px;
    13511686}
    13521687
    1353 .wp-customizer .theme-browser .theme {
     1688.customize-themes-section .filter-drawer {
     1689        border-top: none;
     1690        display: block;
     1691        background: transparent;
     1692        padding-top: 5px;
     1693}
     1694
     1695.customize-themes-section .clear-filters {
     1696        margin-left: 8px;
     1697        display: none;
     1698}
     1699
     1700.customize-themes-section .no-themes {
     1701        display: none;
     1702}
     1703
     1704.themes-section-installed_themes .theme .notice-success {
     1705        display: none; /* Hide "installed" notice on installed themes tab. */
     1706}
     1707
     1708.control-panel-themes .theme-browser .theme .theme-actions .button-primary {
     1709        margin: 0 0 0 8px;
     1710}
     1711
     1712.customize-control-theme .theme {
     1713        width: 100%;
    13541714        margin: 0;
    1355         width: 100%;
    13561715}
    13571716
     1717.customize-control.customize-control-theme { /* override most properties on .customize-control */
     1718        -webkit-box-sizing: border-box;
     1719        -moz-box-sizing: border-box;
     1720        box-sizing: border-box;
     1721        width: 18.4%;
     1722        margin: 0 2% 2% 0;
     1723        padding: 0;
     1724        clear: none;
     1725}
     1726
     1727/* 5 columns above 2100px */
     1728@media screen and (min-width: 2101px) {
     1729        .customize-control.customize-control-theme:nth-child(5n) {
     1730                margin-right: 0;
     1731        }
     1732}
     1733
     1734/* 4 columns up to 2100px */
     1735@media screen and (min-width: 1601px) and (max-width: 2100px) {
     1736        .customize-control.customize-control-theme {
     1737                width: 23.5%;
     1738        }
     1739
     1740        .customize-control.customize-control-theme:nth-child(4n) {
     1741                margin-right: 0;
     1742        }
     1743}
     1744
     1745/* 3 columns up to 1600px */
     1746@media screen and (min-width: 1201px) and (max-width: 1600px) {
     1747        .customize-control.customize-control-theme {
     1748                width: 32%;
     1749        }
     1750
     1751        .customize-control.customize-control-theme:nth-child(3n) {
     1752                margin-right: 0;
     1753        }
     1754}
     1755
     1756/* 2 columns up to 1200px */
     1757@media screen and (min-width: 851px) and (max-width: 1200px) {
     1758        .customize-control.customize-control-theme {
     1759                width: 49%;
     1760        }
     1761
     1762        .customize-control.customize-control-theme:nth-child(even) {
     1763                margin-right: 0;
     1764        }
     1765}
     1766
     1767/* 1 column up to 850 px */
     1768@media screen and (max-width: 850px) {
     1769        .customize-control.customize-control-theme {
     1770                width: 100%;
     1771                margin: 0 0 3% 0;
     1772        }
     1773}
     1774
     1775.wp-customizer .theme-browser .themes {
     1776        padding-bottom: 8px;
     1777}
     1778
    13581779.wp-customizer .theme-browser .theme .theme-actions {
    1359         -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
    13601780        opacity: 1;
    13611781}
    13621782
     
    13751795        width: 100%;
    13761796}
    13771797
    1378 .control-section-themes .accordion-section-title:after,
    1379 .customize-themes-panel .accordion-section-title:after {
    1380         display: none;
    1381 }
    1382 
    1383 .customize-themes-panel.control-panel-content {
    1384         border-top: 1px solid #ddd;
    1385 }
    1386 
    13871798/* Details View */
    13881799.wp-customizer .theme-overlay {
    13891800        display: none;
     
    13981809        z-index: 109;
    13991810}
    14001811
     1812/* Avoid a z-index war by resetting elements that should be under the overlay.
     1813   This is likely required because of the way that sections and panels are positioned. */
     1814.wp-customizer.modal-open #customize-header-actions,
     1815.wp-customizer.modal-open .control-panel-themes .filter-themes-count,
     1816.wp-customizer.modal-open .control-panel-themes .customize-themes-section-title.selected:after {
     1817        z-index: -1;
     1818}
     1819
    14011820.wp-customizer .theme-overlay .theme-backdrop {
    14021821        background: rgba( 238, 238, 238, 0.75 );
    14031822        position: fixed;
     
    14041823        z-index: 110;
    14051824}
    14061825
     1826.wp-customizer .theme-overlay .star-rating {
     1827        float: left;
     1828        margin-right: 8px;
     1829}
     1830
     1831.wp-customizer .theme-rating .num-ratings {
     1832        line-height: 20px;
     1833}
     1834
    14071835.wp-customizer .theme-overlay .theme-wrap {
    14081836        left: 90px;
    14091837        right: 90px;
     
    14101838        top: 45px;
    14111839        bottom: 45px;
    14121840        z-index: 120;
    1413         max-width: 1740px; /* To ensure that theme screenshots are not displayed larger than 880px wide. */
    14141841}
    14151842
    14161843.wp-customizer .theme-overlay .theme-actions {
    1417         text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */
     1844        text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */
     1845        padding: 10px 15px;
    14181846}
    14191847
    1420 .ie8 .wp-customizer .theme-overlay .theme-header,
    1421 .ie8 .wp-customizer .theme-overlay .theme-about,
    1422 .ie8 .wp-customizer .theme-overlay .theme-actions {
    1423         position: static;
     1848.wp-customizer .theme-overlay .theme-actions .theme-install.preview {
     1849        margin-left: 8px;
    14241850}
    14251851
     1852.control-panel-themes .theme-actions .delete-theme {
     1853        left: 15px; /* these override themes.css on mobile */
     1854        right: auto;
     1855        bottom: auto;
     1856        position: absolute;
     1857}
     1858
     1859.modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content {
     1860        overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */
     1861}
     1862
     1863
    14261864/* Small Screens */
    14271865@media (max-width:850px), (max-height:472px) {
    14281866        .wp-customizer .theme-overlay .theme-wrap {
     
    14481886body.cheatin h1 {
    14491887        border-bottom: 1px solid #ddd;
    14501888        clear: both;
    1451         color: #666;
     1889        color: #555d66;
    14521890        font-size: 24px;
    14531891        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
    14541892        margin: 30px 0 0 0;
  • src/wp-admin/css/themes.css

     
    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
     
    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;
     
    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}
     
    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;
     
    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

     
    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

     
    570570
    571571                $parent = false;
    572572                if ( $theme->parent() ) {
    573                         $parent = $theme->parent()->display( 'Name' );
    574                         $parents[ $slug ] = $theme->parent()->get_stylesheet();
     573                        $parent = $theme->parent();
     574                        $parents[ $slug ] = $parent->get_stylesheet();
     575                        $parent = $parent->display( 'Name' );
    575576                }
    576577
    577578                $customize_action = null;
     
    631632 * @since 4.2.0
    632633 */
    633634function 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 );
    636635        ?>
    637636        <script type="text/html" id="tmpl-customize-themes-details-view">
    638637                <div class="theme-backdrop"></div>
     
    644643                        </div>
    645644                        <div class="theme-about wp-clearfix">
    646645                                <div class="theme-screenshots">
    647                                 <# if ( data.screenshot[0] ) { #>
     646                                <# if ( data.screenshot && data.screenshot[0] ) { #>
    648647                                        <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div>
    649648                                <# } else { #>
    650649                                        <div class="screenshot blank"></div>
     
    657656                                        <# } #>
    658657                                        <h2 class="theme-name">{{{ data.name }}}<span class="theme-version"><?php printf( __( 'Version: %s' ), '{{ data.version }}' ); ?></span></h2>
    659658                                        <h3 class="theme-author"><?php printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' ); ?></h3>
    660                                         <p class="theme-description">{{{ data.description }}}</p>
    661659
     660                                        <# if ( data.stars && 0 != data.num_ratings ) { #>
     661                                                <div class="theme-rating">
     662                                                        {{{ data.stars }}}
     663                                                        <span class="num-ratings"><?php echo sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ); ?></span>
     664                                                </div>
     665                                        <# } #>
     666
     667                                        <# if ( data.hasUpdate ) { #>
     668                                                <div class="notice notice-warning notice-alt notice-large" data-slug="{{ data.id }}">
     669                                                        <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
     670                                                        {{{ data.update }}}
     671                                                </div>
     672                                        <# } #>
     673
    662674                                        <# if ( data.parent ) { #>
    663675                                                <p class="parent-theme"><?php printf( __( 'This is a child theme of %s.' ), '<strong>{{{ data.parent }}}</strong>' ); ?></p>
    664676                                        <# } #>
    665677
     678                                        <p class="theme-description">{{{ data.description }}}</p>
     679
    666680                                        <# if ( data.tags ) { #>
    667                                                 <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{ data.tags }}</p>
     681                                                <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
    668682                                        <# } #>
    669683                                </div>
    670684                        </div>
    671685
    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                         <# } #>
     686                        <div class="theme-actions">
     687                                <# if ( data.active ) { #>
     688                                        <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></a>
     689                                <# } else if ( 'installed' === data.type ) { #>
     690                                        <?php if ( current_user_can( 'delete_themes' ) ) { ?>
     691                                                <# if ( data.actions && data.actions['delete'] ) { #>
     692                                                        <a href="{{{ data.actions['delete'] }}}" data-slug="{{ data.id }}" class="button button-secondary delete-theme"><?php _e( 'Delete' ); ?></a>
     693                                                <# } #>
     694                                        <?php } ?>
     695                                        <button type="button" class="button button-primary preview-theme" data-slug="{{ data.id }}"><?php _e( 'Live Preview' ); ?></span>
     696                                <# } else { #>
     697                                        <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
     698                                        <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}"><?php _e( 'Install & Preview' ); ?></button>
     699                                <# } #>
     700                        </div>
    683701                </div>
    684702        </script>
    685703        <?php
  • src/wp-admin/js/customize-controls.js

     
    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                nextTerm: '',
     1331                filterContainer: $(),
    13261332
    13271333                /**
    1328                  * @since 4.2.0
     1334                 * Embed the section in the DOM when the themes panel is ready.
     1335                 *
     1336                 * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
     1337                 *
     1338                 * @since 4.9.0
    13291339                 */
    1330                 initialize: function () {
    1331                         this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
    1332                         return api.Section.prototype.initialize.apply( this, arguments );
     1340                embed: function () {
     1341                        var inject,
     1342                                section = this,
     1343                                container = $( '#customize-theme-controls' );
     1344
     1345                        // Watch for changes to the panel state
     1346                        inject = function ( panelId ) {
     1347                                var parentContainer;
     1348                                api.panel( panelId, function ( panel ) {
     1349                                        // The panel has been registered, wait for it to become ready/initialized
     1350                                        panel.deferred.embedded.done( function () {
     1351                                                parentContainer = panel.contentContainer;
     1352                                                if ( ! section.headContainer.parent().is( parentContainer ) ) {
     1353                                                        parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
     1354                                                }
     1355                                                if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
     1356                                                        container.append( section.contentContainer );
     1357                                                }
     1358                                                section.deferred.embedded.resolve();
     1359                                        });
     1360                                } );
     1361                        };
     1362                        section.panel.bind( inject );
     1363                        inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
    13331364                },
    13341365
    13351366                /**
     
    13631394                                }
    13641395                        });
    13651396
    1366                         _.bindAll( this, 'renderScreenshots' );
     1397                        _.bindAll( this, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
    13671398                },
    13681399
    13691400                /**
     
    13701401                 * Override Section.isContextuallyActive method.
    13711402                 *
    13721403                 * 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.
     1404                 * use the section's own active state instead. This prevents empty search
     1405                 * results for theme sections from causing the section to become inactive.
    13751406                 *
    13761407                 * @since 4.2.0
    13771408                 *
     
    13961427                                section.collapse();
    13971428                        });
    13981429
    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;
     1430                        section.filterContainer = $( '#accordion-section-' + section.id );
     1431
     1432                        // Expand section/panel. Only collapse when opening another section.
     1433                        section.filterContainer.on( 'click', '.customize-themes-section-title', function() {
     1434
     1435                                // Toggle filters.
     1436                                if ( section.filterContainer.find( '.filter-details' ).length ) {
     1437                                        section.filterContainer.find( '.customize-themes-section-title' )
     1438                                                .toggleClass( 'details-open' )
     1439                                                .attr('aria-expanded', function ( i, attr ) {
     1440                                                        return attr === 'true' ? 'false' : 'true';
     1441                                                });
     1442                                        section.filterContainer.find( '.filter-details' ).slideToggle( 180 );
    14031443                                }
    1404                                 event.preventDefault(); // Keep this AFTER the key filter above
    14051444
    1406                                 if ( section.expanded() ) {
    1407                                         section.collapse();
    1408                                 } else {
    1409                                         section.expand();
     1445                                // Open the section.
     1446                                if ( ! section.expanded() ) {
     1447
     1448                                        // Don't expand if there's nothing to show.
     1449                                        if ( -1 !== $.inArray( section.params.action, [ 'search', 'favorites', 'feature_filter' ] ) && '' === section.term ) {
     1450                                                return;
     1451                                        } else {
     1452                                                section.expand();
     1453                                        }
    14101454                                }
    14111455                        });
    14121456
    1413                         // Theme navigation in details view.
    1414                         section.container.on( 'click keydown', '.left', function( event ) {
    1415                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1416                                         return;
    1417                                 }
     1457                        // Preview installed themes.
     1458                        section.container.on( 'click', '.theme-actions .preview-theme', function() {
     1459                                var themeId = $( this ).data( 'slug' );
    14181460
    1419                                 event.preventDefault(); // Keep this AFTER the key filter above
     1461                                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     1462                                api.panel( 'themes' ).loadThemePreview( themeId ).fail( function() {
     1463                                        $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     1464                                } );
     1465                        });
    14201466
     1467                        // Theme navigation in details view.
     1468                        section.container.on( 'click', '.left', function() {
    14211469                                section.previousTheme();
    14221470                        });
    14231471
    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 
     1472                        section.container.on( 'click', '.right', function() {
    14311473                                section.nextTheme();
    14321474                        });
    14331475
    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 
     1476                        section.container.on( 'click', '.theme-backdrop, .close', function() {
    14411477                                section.closeDetails();
    14421478                        });
    14431479
    14441480                        var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
    1445                         section.container.on( 'input', '#themes-filter', function( event ) {
     1481
     1482                        // Only used when there is only one section - installed themes.
     1483                        $( '.control-panel-themes' ).on( 'input', '#themes-filter', function( event ) {
    14461484                                var count,
    14471485                                        term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
    14481486                                        controls = section.controls();
     
    14541492                                renderScreenshots();
    14551493
    14561494                                // Update theme count.
    1457                                 count = section.container.find( 'li.customize-control:visible' ).length;
    1458                                 section.container.find( '.theme-count' ).text( count );
     1495                                count = section.contentContainer.find( 'li.customize-control:visible' ).length;
     1496                                $( '.control-panel-themes' ).find( '.theme-count' ).text( count );
    14591497                        });
    14601498
    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;
     1499                        // Event listeners for queries with user-entered terms.
     1500                        if ( 'search' === section.params.action ) {
     1501                                var debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
     1502                                $( '.control-panel-themes' ).on( 'input', '#wp-filter-search-input', function() {
     1503                                        debounced( section );
     1504                                        if ( ! section.expanded() ) {
     1505                                                section.expand();
    14681506                                        }
    14691507                                });
     1508
     1509                                // Focus the input if the icon is clicked.
     1510                                section.filterContainer.find( '.search-form' ).on( 'click', function( e ) {
     1511                                        if ( ! $( e.currentTarget ).hasClass( 'wp-filter-search' ) ) {
     1512                                                $( e.currentTarget ).find( '.wp-filter-search' ).focus();
     1513                                        }
     1514                                });
     1515                        } else if ( 'favorites' === section.params.action ) {
     1516                                section.checkTerm( section ); // Expand the section if there's already a term.
     1517                                section.container.on( 'click', '.favorites-form-submit', function() {
     1518                                        section.checkTerm( section );
     1519                                });
     1520                                section.container.on( 'keydown', '#wporg-username-input', function( e ) {
     1521                                        if ( api.utils.isKeydownButNotEnterEvent( e ) ) {
     1522                                                return;
     1523                                        }
     1524                                        section.checkTerm( section );
     1525                                });
     1526                        } else if ( 'feature_filter' === section.params.action ) {
     1527                                section.checkTerm( section ); // Expand the section if there's already a term.
     1528                                section.container.on( 'click', '.filter-group input', function() {
     1529                                        section.filtersChecked();
     1530                                        section.checkTerm( section );
     1531                                });
     1532
     1533                                // Toggle feature filter sections.
     1534                                section.container.on( 'click', '.filter-group legend button', function( e ) {
     1535                                        $( e.currentTarget )
     1536                                                .toggleClass( 'open' )
     1537                                                .attr('aria-expanded', function ( i, attr ) {
     1538                                                        return attr === 'true' ? 'false' : 'true';
     1539                                                })
     1540                                                .parent().next( '.filter-group-feature' ).slideToggle( 180 );
     1541                                });
     1542                        }
     1543
     1544                        // Move section controls to the themes area.
     1545                        api.bind( 'ready', function () {
     1546                                section.contentContainer = section.container.find( '.customize-themes-section' );
     1547                                section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
     1548                                section.container.add( section.filterContainer );
    14701549                        });
    14711550                },
    14721551
     
    14781557                 * @param {Boolean}  expanded
    14791558                 * @param {Object}   args
    14801559                 * @param {Boolean}  args.unchanged
    1481                  * @param {Callback} args.completeCallback
     1560                 * @param {Function} args.completeCallback
    14821561                 */
    14831562                onChangeExpanded: function ( expanded, args ) {
    14841563
     
    14911570                        }
    14921571
    14931572                        // 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' );
     1573                        var section = this,
     1574                                container = section.contentContainer.closest( '.customize-themes-full-container' );
    15001575
    1501                         if ( expanded && ! section.hasClass( 'current-panel' ) ) {
     1576                        if ( expanded ) {
     1577
     1578                                if ( -1 !== $.inArray( section.params.action, [ 'search', 'favorites', 'feature_filter' ] ) && '' === section.term ) {
     1579                                        section.collapse(); // Note that the current section hasn't been collapsed yet, so this is all we need to do to do nothing.
     1580                                        return; // Don't expand to an empty section that can't load any themes.
     1581                                }
     1582
     1583                                // Load controls if none are loaded yet.
     1584                                if ( 0 === section.loaded ) {
     1585                                        section.loadControls();
     1586                                }
     1587
    15021588                                // Collapse any sibling sections/panels
    15031589                                api.section.each( function ( otherSection ) {
    1504                                         if ( otherSection !== panel ) {
     1590                                        if ( otherSection !== section ) {
    15051591                                                otherSection.collapse( { duration: args.duration } );
    15061592                                        }
    15071593                                });
    1508                                 api.panel.each( function ( otherPanel ) {
    1509                                         otherPanel.collapse( { duration: 0 } );
    1510                                 });
    15111594
    1512                                 panel._animateChangeExpanded( function() {
    1513                                         changeBtn.attr( 'tabindex', '-1' );
    1514                                         customizeBtn.attr( 'tabindex', '0' );
     1595                                section.contentContainer.addClass( 'current-section' );
     1596                                container.scrollTop();
     1597                                section.filterContainer.find( '.customize-themes-section-title' ).addClass( 'selected' );
    15151598
    1516                                         customizeBtn.focus();
    1517                                         section.css( 'top', '' );
    1518                                         container.scrollTop( 0 );
     1599                                container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
     1600                                container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
    15191601
    1520                                         if ( args.completeCallback ) {
    1521                                                 args.completeCallback();
    1522                                         }
    1523                                 } );
     1602                                if ( args.completeCallback ) {
     1603                                        args.completeCallback();
     1604                                }
     1605                                section.updateCount(); // Show this section's count.
     1606                        } else {
     1607                                section.contentContainer.removeClass( 'current-section' );
    15241608
    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 ) );
     1609                                // Always hide, even if they don't exist or are already hidden.
     1610                                section.filterContainer.find( '.customize-themes-section-title' ).removeClass( 'selected details-open' ).attr( 'aria-expanded', 'false' );
     1611                                section.filterContainer.find( '.filter-details' ).slideUp( 180 );
    15291612
    1530                         } else if ( ! expanded && section.hasClass( 'current-panel' ) ) {
    1531                                 panel._animateChangeExpanded( function() {
    1532                                         changeBtn.attr( 'tabindex', '0' );
    1533                                         customizeBtn.attr( 'tabindex', '-1' );
     1613                                container.off( 'scroll' );
    15341614
    1535                                         changeBtn.focus();
    1536                                         section.css( 'top', '' );
     1615                                if ( args.completeCallback ) {
     1616                                        args.completeCallback();
     1617                                }
     1618                        }
     1619                },
    15371620
    1538                                         if ( args.completeCallback ) {
    1539                                                 args.completeCallback();
     1621                /**
     1622                 * Return the section's content element without detachng from the parent.
     1623                 *
     1624                 * @since 4.9.0
     1625                 */
     1626                getContent: function() {
     1627                        return this.container.find( '.control-section-content' );
     1628                },
     1629
     1630                /**
     1631                 * Load theme data via ajax and add themes to the section as controls.
     1632                 *
     1633                 * @since 4.9.0
     1634                 */
     1635                loadControls: function() {
     1636                        var section = this, params, page, request;
     1637
     1638                        if ( section.loading ) {
     1639                                return; // We're already loading a batch of themes.
     1640                        }
     1641
     1642                        // Parameters for every API query. Additional params are set in PHP.
     1643                        page = Math.ceil( section.loaded / 100 ) + 1;
     1644                        params = {
     1645                                'switch-themes-nonce': api.settings.nonce['switch-themes'],
     1646                                'wp_customize': 'on',
     1647                                'theme_action': section.params.action,
     1648                                'customized_theme': api.settings.theme.stylesheet,
     1649                                'page': page
     1650                        };
     1651
     1652                        // Add fields for special request actions.
     1653                        if ( 'search' === section.params.action ) {
     1654                                if ( '' === section.term ) {
     1655                                        return;
     1656                                } else {
     1657                                        params.search = section.term;
     1658                                }
     1659                        } else if ( 'favorites' === section.params.action ) {
     1660                                if ( '' === section.term ) {
     1661                                        return;
     1662                                } else {
     1663                                        params.user = section.term;
     1664                                }
     1665                        } else if ( 'feature_filter' === section.params.action ) {
     1666                                if ( '' === section.term ) {
     1667                                        return;
     1668                                } else {
     1669                                        params.tags = section.term;
     1670                                }
     1671                        }
     1672
     1673                        // Load themes.
     1674                        section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
     1675                        section.loading = true;
     1676                        section.container.find( '.no-themes' ).hide();
     1677                        request = wp.ajax.post( 'customize-load-themes', params );
     1678                        request.done(function( data ) {
     1679                                var themes = data.themes,
     1680                                    themeControl, newThemeControls;
     1681
     1682                                // Stop and try again if the term changed.
     1683                                if ( section.nextTerm ) {
     1684                                        section.term = section.nextTerm;
     1685                                        section.nextTerm = '';
     1686                                        section.loading = false;
     1687                                        section.loadControls();
     1688                                        return;
     1689                                }
     1690
     1691                                if ( 0 !== themes.length ) {
     1692                                        newThemeControls = [];
     1693                                        // Add controls for each theme.
     1694                                        _.each( themes, function ( theme ) {
     1695                                                var customizeId = section.params.action + '_theme_' + theme.id;
     1696                                                themeControl = new api.controlConstructor.theme( customizeId, {
     1697                                                        params: {
     1698                                                                type: 'theme',
     1699                                                                content: '<li id="customize-control-theme-' + section.params.action + '_' + theme.id + '" class="customize-control customize-control-theme"></li>',
     1700                                                                section: section.params.id,
     1701                                                                active: true,
     1702                                                                theme: theme,
     1703                                                                priority: section.loaded + 1
     1704                                                        },
     1705                                                        previewer: api.previewer
     1706                                                } );
     1707
     1708                                                api.control.add( customizeId, themeControl );
     1709                                                newThemeControls.push( themeControl );
     1710                                                section.loaded = section.loaded + 1;
     1711                                        });
     1712
     1713                                        if ( 1 === page ) {
     1714                                                // Pre-load the first 3 theme screenshots.
     1715                                                _.each( section.controls().slice( 0, 3 ), function ( control ) {
     1716                                                        var img, src = control.params.theme.screenshot[0];
     1717                                                        if ( src ) {
     1718                                                                img = new Image();
     1719                                                                img.src = src;
     1720                                                        }
     1721                                                });
     1722                                                if ( 'search' === section.params.action ) {
     1723                                                        wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
     1724                                                }
     1725                                        } else {
     1726                                                Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
    15401727                                        }
    1541                                 } );
     1728                                        _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
    15421729
    1543                                 overlay.removeClass( 'in-themes-panel' );
    1544                                 section.removeClass( 'current-panel' );
    1545                                 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
     1730                                        if ( 'installed' === section.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
     1731                                                section.fullyLoaded = true;
     1732                                        }
     1733                                } else {
     1734                                        if ( 0 === section.loaded ) {
     1735                                                section.container.find( '.no-themes' ).show();
     1736                                                wp.a11y.speak( section.container.find( '.no-themes' ).text() );
     1737                                        } else {
     1738                                                section.fullyLoaded = true;
     1739                                        }
     1740                                }
     1741                                if ( 'installed' === section.params.action ) {
     1742                                        section.updateCount();
     1743                                } else {
     1744                                        section.updateCount( data.info.results );
     1745                                }
     1746                                section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
     1747
     1748                                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1749                                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1750                                section.loading = false;
     1751                        });
     1752                        request.fail(function( data ) {
     1753                                if ( 'undefined' === typeof data ) {
     1754                                        section.container.find( '.unexpected-error' ).show();
     1755                                        wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
     1756                                } else if ( typeof console !== 'undefined' && console.error ) {
     1757                                        console.error( data );
     1758                                }
     1759
     1760                                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1761                                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1762                                section.loading = false;
     1763                        });
     1764                },
     1765
     1766                /**
     1767                 * Determines whether more themes should be loaded, and loads them.
     1768                 *
     1769                 * @since 4.9.0
     1770                 */
     1771                loadMore: function() {
     1772                        var section = this, container, bottom, threshold;
     1773                        if ( ! section.fullyLoaded && ! section.loading ) {
     1774                                container = section.container.closest( '.customize-themes-full-container' );
     1775
     1776                                bottom = container.scrollTop() + container.height();
     1777                                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.
     1778
     1779                                if ( bottom > threshold ) {
     1780                                        section.loadControls();
     1781                                }
    15461782                        }
    15471783                },
    15481784
    15491785                /**
     1786                 * Event handler for search, feature filter, and favorites input that determines if the term has changed and loads new controls as needed.
     1787                 *
     1788                 * @since 4.9.0
     1789                 *
     1790                 * @param {wp.customize.ThemesSection} section The current theme section, passed through the debouncer.
     1791                 */
     1792                checkTerm: function( section ) {
     1793                        var newTerm;
     1794
     1795                        // Find term.
     1796                        if ( 'search' === section.params.action ) {
     1797                                newTerm = $( '#wp-filter-search-input' ).val();
     1798                        } else if ( 'favorites' === section.params.action ) {
     1799                                newTerm = $( '#wporg-username-input' ).val();
     1800                        } else if ( 'feature_filter' === section.params.action ) {
     1801                                newTerm = section.term; // Set separately by filtersChecked(), as they're changed.
     1802                                if ( '' === newTerm ) {
     1803                                        return;
     1804                                }
     1805                        } else {
     1806                                return;
     1807                        }
     1808
     1809                        if ( section.term === newTerm && 'feature_filter' !== section.params.action ) {
     1810                                return;
     1811                        }
     1812
     1813                        // Clear the controls in the section.
     1814                        _.each( section.controls(), function( control ) {
     1815                                control.container.remove();
     1816                                api.control.remove( control.id );
     1817                        });
     1818                        section.loaded = 0;
     1819                        section.fullyLoaded = false;
     1820                        section.screenshotQueue = null;
     1821
     1822                        if ( '' !== newTerm ) { // Empty term should not show any results.
     1823                                // Run a new query, with loadControls handling paging, etc.
     1824                                section.term = newTerm;
     1825                                if ( ! section.loading ) {
     1826                                        section.loadControls();
     1827                                } else {
     1828                                        section.nextTerm = newTerm; // This will reload with the newest term once the current batch is loaded.
     1829                                }
     1830                                if ( ! section.expanded() ) {
     1831                                        section.expand(); // Expand the section if it isn't expanded.
     1832                                }
     1833                        }
     1834                },
     1835
     1836                /**
     1837                 * Check for filters checked in the feature filter list.
     1838                 *
     1839                 * @since 4.9.0
     1840                 */
     1841                filtersChecked: function() {
     1842                        var section = this,
     1843                            items = section.container.find( '.filter-group' ).find( ':checkbox' ),
     1844                            tags = [];
     1845
     1846                        if ( 'feature_filter' !== section.params.action ) {
     1847                                return false;
     1848                        }
     1849
     1850                        _.each( items.filter( ':checked' ), function( item ) {
     1851                                tags.push( $( item ).prop( 'value' ) );
     1852                        });
     1853
     1854                        // When no filters are checked, restore initial state and return
     1855                        if ( tags.length === 0 ) {
     1856                                section.term = '';
     1857                        } else {
     1858                                section.term = tags;
     1859                        }
     1860                },
     1861
     1862                /**
    15501863                 * Render control's screenshot if the control comes into view.
    15511864                 *
    15521865                 * @since 4.2.0
     
    15541867                renderScreenshots: function( ) {
    15551868                        var section = this;
    15561869
    1557                         // Fill queue initially.
    1558                         if ( section.screenshotQueue === null ) {
    1559                                 section.screenshotQueue = section.controls();
     1870                        // Fill queue initially, or check for more if empty.
     1871                        if ( section.screenshotQueue === null || 0 === section.screenshotQueue.length ) {
     1872                                // Add controls that haven't had their screenshots rendered.
     1873                                section.screenshotQueue = _.filter( section.controls(), function( control ) {
     1874                                        return ! control.screenshotRendered;
     1875                                });
    15601876                        }
    15611877
    1562                         // Are all screenshots rendered?
     1878                        // Are all screenshots rendered (for now)?
    15631879                        if ( ! section.screenshotQueue.length ) {
    15641880                                return;
    15651881                        }
     
    15951911                },
    15961912
    15971913                /**
     1914                 * Update the number of themes in the section.
     1915                 *
     1916                 * @since 4.9.0
     1917                 */
     1918                updateCount: function ( count ) {
     1919                        if ( ! count ) {
     1920                                count = this.loaded;
     1921                        }
     1922
     1923                        var displayed = this.container.closest( '.control-panel-content' ).find( '.themes-displayed' ),
     1924                            countEl = this.container.closest( '.control-panel-content' ).find( '.theme-count' );
     1925
     1926                        if ( 0 === count ) {
     1927                                countEl.text( count );
     1928                        } else {
     1929                                // Animate the count change for emphasis.
     1930                                displayed.fadeOut( 180, function() {
     1931                                        countEl.text( count );
     1932                                        displayed.fadeIn( 180 );
     1933                                } );
     1934                                wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
     1935                        }
     1936                },
     1937
     1938                /**
    15981939                 * Advance the modal to the next theme.
    15991940                 *
    16001941                 * @since 4.2.0
     
    16141955                 * @since 4.2.0
    16151956                 */
    16161957                getNextTheme: function () {
    1617                         var control, next;
    1618                         control = api.control( 'theme_' + this.currentTheme );
     1958                        var section = this, control, next;
     1959                        control = api.control( section.params.action + '_theme_' + this.currentTheme );
    16191960                        next = control.container.next( 'li.customize-control-theme' );
    16201961                        if ( ! next.length ) {
    16211962                                return false;
    16221963                        }
    1623                         next = next[0].id.replace( 'customize-control-', '' );
     1964                        next = next[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    16241965                        control = api.control( next );
    16251966
    16261967                        return control.params.theme;
     
    16461987                 * @since 4.2.0
    16471988                 */
    16481989                getPreviousTheme: function () {
    1649                         var control, previous;
    1650                         control = api.control( 'theme_' + this.currentTheme );
     1990                        var section = this, control, previous;
     1991                        control = api.control( section.params.action + '_theme_' + this.currentTheme );
    16511992                        previous = control.container.prev( 'li.customize-control-theme' );
    16521993                        if ( ! previous.length ) {
    16531994                                return false;
    16541995                        }
    1655                         previous = previous[0].id.replace( 'customize-control-', '' );
     1996                        previous = previous[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    16561997                        control = api.control( previous );
    16571998
    16581999                        return control.params.theme;
     
    17342075                 * @param {Object}   theme
    17352076                 */
    17362077                showDetails: function ( theme, callback ) {
    1737                         var section = this, link;
     2078                        var section = this;
    17382079                        callback = callback || function(){};
    17392080                        section.currentTheme = theme.id;
    17402081                        section.overlay.html( section.template( theme ) )
     
    17432084                        $( 'body' ).addClass( 'modal-open' );
    17442085                        section.containFocus( section.overlay );
    17452086                        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                         } );
     2087                        wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
    17622088                        callback();
    17632089                },
    17642090
     
    17702096                closeDetails: function () {
    17712097                        $( 'body' ).removeClass( 'modal-open' );
    17722098                        this.overlay.fadeOut( 'fast' );
    1773                         api.control( 'theme_' + this.currentTheme ).focus();
     2099                        api.control( this.params.action + '_theme_' + this.currentTheme ).container.find( '.theme' ).focus();
    17742100                },
    17752101
    17762102                /**
     
    18502176                        }
    18512177                        if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
    18522178                                container.append( panel.contentContainer );
    1853                                 panel.renderContent();
    18542179                        }
     2180                        panel.renderContent();
    18552181
    18562182                        panel.deferred.embedded.resolve();
    18572183                },
     
    20612387                }
    20622388        });
    20632389
     2390
    20642391        /**
     2392         * wp.customize.ThemesPanel
     2393         *
     2394         * Custom section for themes that displays without the customize preview.
     2395         *
     2396         * @constructor
     2397         * @augments wp.customize.Panel
     2398         * @augments wp.customize.Container
     2399         */
     2400        api.ThemesPanel = api.Panel.extend({
     2401                installingThemes: [],
     2402
     2403                /**
     2404                 * @since 4.9.0
     2405                 */
     2406                attachEvents: function () {
     2407                        var panel = this;
     2408
     2409                        // Attach regular panel events.
     2410                        api.Panel.prototype.attachEvents.apply( this );
     2411
     2412                        // Collapse panel to customize the current theme.
     2413                        panel.contentContainer.on( 'click', '.customize-theme', function() {
     2414                                panel.collapse();
     2415                        });
     2416
     2417                        // Toggle between filtering and browsing themes on mobile.
     2418                        panel.contentContainer.on( 'click', '.see-themes, .filter-themes', function() {
     2419                                $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
     2420                        });
     2421
     2422                        // Install (and maybe preview) a theme.
     2423                        panel.contentContainer.on( 'click', '.theme-install', function( event ) {
     2424                                panel.installTheme( event );
     2425                        });
     2426
     2427                        // Update a theme. Theme cards have the class, the details modal has the id.
     2428                        panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
     2429                                // #update-theme is a link.
     2430                                event.preventDefault();
     2431                                event.stopPropagation();
     2432
     2433                                panel.updateTheme( event );
     2434                        });
     2435
     2436                        // Delete a theme.
     2437                        panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
     2438                                panel.deleteTheme( event );
     2439                        });
     2440
     2441                        _.bindAll( this, 'installTheme', 'updateTheme' );
     2442                },
     2443
     2444                /**
     2445                 * Update UI to reflect expanded state
     2446                 *
     2447                 * @since 4.9.0
     2448                 *
     2449                 * @param {Boolean}  expanded
     2450                 * @param {Object}   args
     2451                 * @param {Boolean}  args.unchanged
     2452                 * @param {Function} args.completeCallback
     2453                 */
     2454                onChangeExpanded: function ( expanded, args ) {
     2455
     2456                        // Expand/collapse the panel normally.
     2457                        api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
     2458
     2459                        // Immediately call the complete callback if there were no changes
     2460                        if ( args.unchanged ) {
     2461                                if ( args.completeCallback ) {
     2462                                        args.completeCallback();
     2463                                }
     2464                                return;
     2465                        }
     2466
     2467                        // Note: there is a second argument 'args' passed
     2468                        var panel = this,
     2469                                overlay = panel.headContainer.closest( '.wp-full-overlay' );
     2470
     2471                        if ( expanded ) {
     2472                                overlay
     2473                                        .addClass( 'in-themes-panel' ).addClass( 'showing-themes' )
     2474                                        .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
     2475
     2476                                // Automatically open the installed themes section.
     2477                                api.section( 'installed_themes' ).expand();
     2478                        } else {
     2479                                overlay
     2480                                        .removeClass( 'in-themes-panel' )
     2481                                        .find( '.customize-themes-full-container' ).removeClass( 'animate' );
     2482                        }
     2483                },
     2484
     2485                /**
     2486                 * Install a theme via wp.updates.
     2487                 *
     2488                 * @since 4.9.0
     2489                 */
     2490                installTheme: function( event ) {
     2491                        var panel = this, preview = false, slug = $( event.target ).data( 'slug' );
     2492
     2493                        if ( -1 !== $.inArray( this.installingThemes, slug ) ) {
     2494                                return; // Theme is already being installed.
     2495                        }
     2496
     2497                        wp.updates.maybeRequestFilesystemCredentials( event );
     2498
     2499                        $( document ).one( 'wp-theme-install-success', function( event, response ) {
     2500                                var theme = false, customizeId, themeControl;
     2501                                if ( preview ) {
     2502
     2503                                        // Update loading message. Everything else is handled by reloading the page.
     2504                                //      $( '#customize-themes-loading-container span' ).hide();
     2505                                //      $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' );
     2506
     2507                                        panel.loadThemePreview( slug ).fail( function() {
     2508                                                $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     2509                                        } );
     2510
     2511                                } else {
     2512                                        api.control.each( function( control ) {
     2513                                                if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2514                                                        theme = control.params.theme; // Used below to add theme control.
     2515                                                        control.rerenderAsInstalled( true );
     2516                                                }
     2517                                        });
     2518
     2519                                        // Don't add the same theme more than once.
     2520                                        if ( ! theme || 'undefined' !== typeof api.control( 'installed_theme_' + theme.id ) ) {
     2521                                                return;
     2522                                        }
     2523
     2524                                        // Add theme control to installed section.
     2525                                        theme.type = 'installed';
     2526                                        customizeId = 'installed_theme_' + theme.id;
     2527                                        themeControl = new api.controlConstructor.theme( customizeId, {
     2528                                                params: {
     2529                                                        type: 'theme',
     2530                                                        content: $( '<li class="customize-control customize-control-theme"></li>' ).attr( 'id', 'customize-control-theme-installed_' + theme.id ).prop( 'outerHTML' ),
     2531                                                        section: 'installed_themes',
     2532                                                        active: true,
     2533                                                        theme: theme,
     2534                                                        priority: 0 // Add all newly-installed themes to the top.
     2535                                                },
     2536                                                previewer: api.previewer
     2537                                        } );
     2538
     2539                                        api.control.add( customizeId, themeControl );
     2540                                        api.control( customizeId ).container.trigger( 'render-screenshot' );
     2541
     2542                                        // Close the details modal if it's open to the installed theme.
     2543                                        api.section.each( function( section ) {
     2544                                                if ( 'themes' === section.params.type ) {
     2545                                                        if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
     2546                                                                section.closeDetails();
     2547                                                        }
     2548                                                }
     2549                                        });
     2550                                }
     2551                        } );
     2552
     2553                        this.installingThemes.push( $( event.target ).data( 'slug' ) ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
     2554                        wp.updates.installTheme( {
     2555                                slug: slug
     2556                        } );
     2557
     2558                        // Also preview the theme as the event is triggered on Install & Preview.
     2559                        if ( $( event.target ).hasClass( 'preview' ) ) {
     2560                                preview = true;
     2561                                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     2562                        }
     2563                },
     2564
     2565                /**
     2566                 * Load theme preview.
     2567                 *
     2568                 * @since 4.9.0
     2569                 *
     2570                 * @param {string} themeId Theme ID.
     2571                 * @returns {jQuery.promise} Promise.
     2572                 */
     2573                loadThemePreview: function( themeId ) {
     2574                        var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
     2575
     2576                        urlParser = document.createElement( 'a' );
     2577                        urlParser.href = location.href;
     2578                        urlParser.search = $.param( _.extend(
     2579                                api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
     2580                                {
     2581                                        theme: themeId,
     2582                                        changeset_uuid: api.settings.changeset.uuid
     2583                                }
     2584                        ) );
     2585
     2586                        // Update loading message. Everything else is handled by reloading the page.
     2587                        $( '#customize-themes-loading-container span' ).hide();
     2588                        $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' );
     2589                        overlay = $( '.wp-full-overlay' );
     2590                        overlay.addClass( 'customize-loading' );
     2591
     2592                        onceProcessingComplete = function() {
     2593                                var request;
     2594                                if ( api.state( 'processing' ).get() > 0 ) {
     2595                                        return;
     2596                                }
     2597
     2598                                api.state( 'processing' ).unbind( onceProcessingComplete );
     2599
     2600                                request = api.requestChangesetUpdate();
     2601                                request.done( function() {
     2602                                        $( window ).off( 'beforeunload.customize-confirm' );
     2603                                        window.location.href = urlParser.href;
     2604                                } );
     2605                                request.fail( function() {
     2606                                        overlay.removeClass( 'customize-loading' );
     2607                                } );
     2608                        };
     2609
     2610                        if ( 0 === api.state( 'processing' ).get() ) {
     2611                                onceProcessingComplete();
     2612                        } else {
     2613                                api.state( 'processing' ).bind( onceProcessingComplete );
     2614                        }
     2615
     2616                        return deferred.promise();
     2617                },
     2618
     2619                /**
     2620                 * Update a theme via wp.updates.
     2621                 *
     2622                 * @since 4.9.0
     2623                 */
     2624                updateTheme: function( event ) {
     2625                        wp.updates.maybeRequestFilesystemCredentials( event );
     2626
     2627                        $( document ).one( 'wp-theme-update-success', function( event, response ) {
     2628                                // Rerender the control to reflect the update.
     2629                                api.control.each( function( control ) {
     2630                                        if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2631                                                control.params.theme.hasUpdate = false;
     2632                                                control.rerenderAsInstalled( true );
     2633                                        }
     2634                                });
     2635                        } );
     2636
     2637                        wp.updates.updateTheme( {
     2638                                slug: $( event.target ).closest( '.notice' ).data( 'slug' )
     2639                        } );
     2640                },
     2641
     2642                /**
     2643                 * Delete a theme via wp.updates.
     2644                 *
     2645                 * @since 4.9.0
     2646                 */
     2647                deleteTheme: function( event ) {
     2648                        var theme, section;
     2649                        theme = $( event.target ).data( 'slug' );
     2650                        section = api.section( 'installed_themes' );
     2651
     2652                        event.preventDefault();
     2653
     2654                        // Confirmation dialog for deleting a theme.
     2655                        if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
     2656                                return;
     2657                        }
     2658
     2659                        wp.updates.maybeRequestFilesystemCredentials( event );
     2660
     2661                        $( document ).one( 'wp-theme-delete-success', function() {
     2662                                var control = api.control( 'installed_theme_' + theme );
     2663
     2664                                // Remove theme control.
     2665                                control.container.remove();
     2666                                api.control.remove( control.id );
     2667
     2668                                // Update installed count.
     2669                                section.loaded = section.loaded - 1;
     2670                                section.updateCount();
     2671
     2672                                // Rerender any other theme controls as uninstalled.
     2673                                api.control.each( function( control ) {
     2674                                        if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
     2675                                                control.rerenderAsInstalled( false );
     2676                                        }
     2677                                });
     2678                        } );
     2679
     2680                        wp.updates.deleteTheme( {
     2681                                slug: theme
     2682                        } );
     2683
     2684                        // Close modal and focus the section.
     2685                        section.closeDetails();
     2686                        section.focus();
     2687                }
     2688
     2689        });
     2690
     2691
     2692        /**
    20652693         * A Customizer Control.
    20662694         *
    20672695         * A control provides a UI element that allows a user to modify a Customizer Setting.
     
    24103038                 * @param {Boolean}  active
    24113039                 * @param {Object}   args
    24123040                 * @param {Number}   args.duration
    2413                  * @param {Callback} args.completeCallback
     3041                 * @param {Function} args.completeCallback
    24143042                 */
    24153043                onChangeActive: function ( active, args ) {
    24163044                        if ( args.unchanged ) {
     
    35824210        api.ThemeControl = api.Control.extend({
    35834211
    35844212                touchDrag: false,
    3585                 isRendered: false,
     4213                screenshotRendered: false,
    35864214
    35874215                /**
    3588                  * Defer rendering the theme control until the section is displayed.
    3589                  *
    35904216                 * @since 4.2.0
    35914217                 */
    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                 },
    3610 
    3611                 /**
    3612                  * @since 4.2.0
    3613                  */
    36144218                ready: function() {
    36154219                        var control = this;
    36164220
     
    36304234                                }
    36314235
    36324236                                // Prevent the modal from showing when the user clicks the action button.
    3633                                 if ( $( event.target ).is( '.theme-actions .button' ) ) {
     4237                                if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
    36344238                                        return;
    36354239                                }
    36364240
    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 ) ) {
    3642                                         return;
    3643                                 }
    3644 
    36454241                                event.preventDefault(); // Keep this AFTER the key filter above
    3646 
    36474242                                api.section( control.section() ).showDetails( control.params.theme );
    36484243                        });
    36494244
     
    36544249                                if ( source ) {
    36554250                                        $screenshot.attr( 'src', source );
    36564251                                }
     4252                                control.screenshotRendered = true;
    36574253                        });
    36584254                },
    36594255
    36604256                /**
    3661                  * Show or hide the theme based on the presence of the term in the title, description, and author.
     4257                 * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
    36624258                 *
    36634259                 * @since 4.2.0
    36644260                 */
     
    36744270                        } else {
    36754271                                control.deactivate();
    36764272                        }
     4273                },
     4274
     4275                /**
     4276                 * Rerender the theme from its JS template with the installed type.
     4277                 *
     4278                 * @since 4.9.0
     4279                 */
     4280                rerenderAsInstalled: function( installed ) {
     4281                        var control = this, section;
     4282                        if ( installed ) {
     4283                                control.params.theme.type = 'installed';
     4284                        } else {
     4285                                section = api.section( control.params.section );
     4286                                control.params.theme.type = section.params.action;
     4287                        }
     4288                        control.renderContent(); // replaces existing content
     4289                        control.container.trigger( 'render-screenshot' );
    36774290                }
    36784291        });
    36794292
     
    43744987                background_position: api.BackgroundPositionControl,
    43754988                theme:               api.ThemeControl
    43764989        };
    4377         api.panelConstructor = {};
     4990        api.panelConstructor = {
     4991                themes: api.ThemesPanel
     4992        };
    43784993        api.sectionConstructor = {
    43794994                themes: api.ThemesSection
    43804995        };
     
    44925107
    44935108                // Sort the sections within each panel
    44945109                api.panel.each( function ( panel ) {
     5110                        if ( 'themes' === panel.id ) {
     5111                                return; // Don't reflow theme sections, as doing so moves them after the themes container.
     5112                        }
     5113
    44955114                        var sections = panel.sections(),
    44965115                                sectionHeadContainers = _.pluck( sections, 'headContainer' );
    44975116                        rootNodes.push( panel );
     
    52145833                        // Collapse the most granular expanded object.
    52155834                        collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
    52165835                        if ( collapsedObject ) {
     5836                                if ( 'themes' === collapsedObject.params.type ) {
     5837                                        // Themes panel or section.
     5838                                        if ( $( 'body' ).hasClass( 'modal-open' ) ) {
     5839                                                collapsedObject.closeDetails();
     5840                                        } else {
     5841                                                // If we're collapsing a section, collapse the panel also.
     5842                                                wp.customize.panel( 'themes' ).collapse();
     5843                                        }
     5844                                        return;
     5845                                }
    52175846                                collapsedObject.collapse();
    52185847                                event.preventDefault();
    52195848                        }
  • src/wp-admin/js/updates.js

     
    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

     
    302302
    303303                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
    304304
     305                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
    305306                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
    306307                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
    307308                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
     
    357358
    358359                add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
    359360                add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
     361                add_action( 'wp_ajax_customize-load-themes',    array( $this, 'load_themes_ajax' ) );
    360362
    361363                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
    362364                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
     
    373375
    374376                // Export the settings to JS via the _wpCustomizeSettings variable.
    375377                add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
     378
     379                // Add theme update notices.
     380                if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
     381                        require_once( ABSPATH . '/wp-admin/includes/update.php' );
     382                        add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
     383                }
    376384        }
    377385
    378386        /**
     
    33433351                                'type' => 'text/css',
    33443352                        ) );
    33453353                }
     3354
     3355                if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
     3356                        wp_enqueue_script( 'updates' );
     3357                }
    33463358        }
    33473359
    33483360        /**
     
    35473559                $nonces = array(
    35483560                        'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
    35493561                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
     3562                        'switch-themes' => wp_create_nonce( 'switch-themes' ),
    35503563                );
    35513564
    35523565                /**
     
    36233636                        'autofocus' => $this->get_autofocus(),
    36243637                        'documentTitleTmpl' => $this->get_document_title_template(),
    36253638                        'previewableDevices' => $this->get_previewable_devices(),
     3639                        'l10n' => array(
     3640                                'confirmDeleteTheme' => __( 'Are you sure you want to delete this theme?' ),
     3641                                /* translators: %d is the number of theme search results, which cannot currently consider singular vs. plural forms */
     3642                                'themeSearchResults' => __( '%d themes found' ),
     3643                                /* translators: %d is the number of themes being displayed, which cannot currently consider singular vs. plural forms */
     3644                                'announceThemeCount' => __( 'Displaying %d themes' ),
     3645                                'announceThemeDetails' => __( 'Showing details for theme: %s' ),
     3646                        ),
    36263647                );
    36273648
    36283649                // Prepare Customize Section objects to pass to JavaScript.
     
    37253746
    37263747                /* Panel, Section, and Control Types */
    37273748                $this->register_panel_type( 'WP_Customize_Panel' );
     3749                $this->register_panel_type( 'WP_Customize_Themes_Panel' );
    37283750                $this->register_section_type( 'WP_Customize_Section' );
    37293751                $this->register_section_type( 'WP_Customize_Sidebar_Section' );
     3752                $this->register_section_type( 'WP_Customize_Themes_Section' );
    37303753                $this->register_control_type( 'WP_Customize_Color_Control' );
    37313754                $this->register_control_type( 'WP_Customize_Media_Control' );
    37323755                $this->register_control_type( 'WP_Customize_Upload_Control' );
     
    37373760                $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
    37383761                $this->register_control_type( 'WP_Customize_Theme_Control' );
    37393762
    3740                 /* Themes */
     3763                /* Themes (controls are loaded via ajax) */
    37413764
    3742                 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
    3743                         'title'      => $this->theme()->display( 'Name' ),
    3744                         'capability' => 'switch_themes',
    3745                         'priority'   => 0,
     3765                $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array(
     3766                        'title'       => $this->theme()->display( 'Name' ),
     3767                        '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.' ),
     3768                        'capability'  => 'switch_themes',
     3769                        'priority'    => 0,
    37463770                ) ) );
    37473771
    3748                 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
    3749                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
    3750                         'capability' => 'switch_themes',
     3772                $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array(
     3773                        'title'       => __( 'Installed' ),
     3774                        'text_before' => __( 'Your local site' ),
     3775                        'action'      => 'installed',
     3776                        'capability'  => 'switch_themes',
     3777                        'panel'       => 'themes',
     3778                        'priority'    => 0,
    37513779                ) ) );
    37523780
    3753                 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
     3781                if ( ! is_multisite() ) {
     3782                        $this->add_section( new WP_Customize_Themes_Section( $this, 'search_themes', array(
     3783                                'title'       => __( 'Search themes&hellip;' ),
     3784                                'text_before' => __( 'Browse all WordPress.org themes' ),
     3785                                'action'      => 'search',
     3786                                'capability'  => 'install_themes',
     3787                                'panel'       => 'themes',
     3788                                'priority'    => 5,
     3789                        ) ) );
    37543790
    3755                 // Theme Controls.
     3791                        $this->add_section( new WP_Customize_Themes_Section( $this, 'featured_themes', array(
     3792                                'title'      => __( 'Featured' ),
     3793                                'action'     => 'featured',
     3794                                'capability' => 'install_themes',
     3795                                'panel'      => 'themes',
     3796                                'priority'   => 10,
     3797                        ) ) );
    37563798
    3757                 // Add a control for the active/original theme.
    3758                 if ( ! $this->is_theme_active() ) {
    3759                         $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
    3760                         $active_theme = current( $themes );
    3761                         $active_theme['isActiveTheme'] = true;
    3762                         $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
    3763                                 'theme'    => $active_theme,
    3764                                 'section'  => 'themes',
    3765                                 'settings' => 'active_theme',
     3799                        $this->add_section( new WP_Customize_Themes_Section( $this, 'popular_themes', array(
     3800                                'title'      => __( 'Popular' ),
     3801                                'action'     => 'popular',
     3802                                'capability' => 'install_themes',
     3803                                'panel'      => 'themes',
     3804                                'priority'   => 15,
    37663805                        ) ) );
    3767                 }
    37683806
    3769                 $themes = wp_prepare_themes_for_js();
    3770                 foreach ( $themes as $theme ) {
    3771                         if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
    3772                                 continue;
    3773                         }
     3807                        $this->add_section( new WP_Customize_Themes_Section( $this, 'latest_themes', array(
     3808                                'title'      => __( 'Latest' ),
     3809                                'action'     => 'latest',
     3810                                'capability' => 'install_themes',
     3811                                'panel'      => 'themes',
     3812                                'priority'   => 20,
     3813                        ) ) );
    37743814
    3775                         $theme_id = 'theme_' . $theme['id'];
    3776                         $theme['isActiveTheme'] = false;
    3777                         $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
    3778                                 'theme'    => $theme,
    3779                                 'section'  => 'themes',
    3780                                 'settings' => 'active_theme',
     3815                        $this->add_section( new WP_Customize_Themes_Section( $this, 'feature_filter_themes', array(
     3816                                'title'      => __( 'Feature Filter' ),
     3817                                'action'     => 'feature_filter',
     3818                                'capability' => 'install_themes',
     3819                                'panel'      => 'themes',
     3820                                'priority'   => 25,
    37813821                        ) ) );
     3822
     3823                        $this->add_section( new WP_Customize_Themes_Section( $this, 'favorites_themes', array(
     3824                                'title'      => __( 'Favorites' ),
     3825                                'action'     => 'favorites',
     3826                                'capability' => 'install_themes',
     3827                                'panel'      => 'themes',
     3828                                'priority'   => 30,
     3829                        ) ) );
    37823830                }
    37833831
     3832                // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
     3833                $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
     3834                        'capability' => 'switch_themes',
     3835                ) ) );
     3836
    37843837                /* Site Identity */
    37853838
    37863839                $this->add_section( 'title_tagline', array(
     
    42924345        }
    42934346
    42944347        /**
     4348         * Load themes into the theme browsing/installation UI.
     4349         *
     4350         * @since 4.9.0
     4351         */
     4352        public function load_themes_ajax() {
     4353                check_ajax_referer( 'switch-themes', 'switch-themes-nonce' );
     4354
     4355                if ( ! current_user_can( 'switch_themes' ) ) {
     4356                        wp_die( -1 );
     4357                }
     4358
     4359                if ( empty( $_POST['theme_action'] ) ) {
     4360                        wp_send_json_error( 'missing_theme_action' );
     4361                }
     4362
     4363                if ( 'search' === $_POST['theme_action'] && ! array_key_exists( 'search', $_POST ) ) {
     4364                        wp_send_json_error( 'empty_search' );
     4365                } elseif ( 'favorites' === $_POST['theme_action'] && ! array_key_exists( 'user', $_POST ) ) {
     4366                        wp_send_json_error( 'empty_user' );
     4367                } elseif ( 'feature_filter' === $_POST['theme_action'] && ! array_key_exists( 'tags', $_POST ) ) {
     4368                        wp_send_json_error( 'no_features' );
     4369                }
     4370
     4371                require_once( ABSPATH . 'wp-admin/includes/theme.php' );
     4372                if ( 'installed' === $_POST['theme_action'] ) {
     4373                        $themes = array( 'themes' => wp_prepare_themes_for_js() );
     4374                        foreach ( $themes['themes'] as &$theme ) {
     4375                                $theme['type'] = 'installed';
     4376                                // Set active based on customized theme.
     4377                                if ( $_POST['customized_theme'] === $theme['id'] ) {
     4378                                        $theme['active'] = true;
     4379                                } else {
     4380                                        $theme['active'] = false;
     4381                                }
     4382                        }
     4383                } else {
     4384                        if ( ! current_user_can( 'install_themes' ) ) {
     4385                                wp_die( -1 );
     4386                        }
     4387
     4388                        // Arguments for all queries.
     4389                        $args = array(
     4390                                'per_page' => 100,
     4391                                'page' => absint( $_POST['page'] ),
     4392                                'fields' => array(
     4393                                        'screenshot_url' => true,
     4394                                        'description' => true,
     4395                                        'rating' => true,
     4396                                        'downloaded' => true,
     4397                                        'downloadlink' => true,
     4398                                        'last_updated' => true,
     4399                                        'homepage' => true,
     4400                                        'num_ratings' => true,
     4401                                        'tags' => true,
     4402                                        'parent' => true,
     4403                                        //'extended_author' => true, @todo: WordPress.org throws a 500 server error when this is here.
     4404                                ),
     4405                        );
     4406
     4407                        // Specialized handling for each query.
     4408                        switch ( $_POST['theme_action'] ) {
     4409                                case 'search':
     4410                                        $args['search'] = wp_unslash( $_POST['search'] );
     4411                                        break;
     4412                                case 'favorites':
     4413                                        $args['user'] = wp_unslash( $_POST['user'] );
     4414                                case 'featured':
     4415                                case 'popular':
     4416                                        $args['browse'] = wp_unslash( $_POST['theme_action'] );
     4417                                        break;
     4418                                case 'latest':
     4419                                        $args['browse'] = 'new';
     4420                                        break;
     4421                                case 'feature_filter':
     4422                                        $args['tag'] = wp_unslash( $_POST['tags'] );
     4423                                        break;
     4424                        }
     4425
     4426                        // Load themes from the .org API.
     4427                        $themes = themes_api( 'query_themes', $args );
     4428                        if ( is_wp_error( $themes ) ) {
     4429                                wp_send_json_error();
     4430                        }
     4431
     4432                        // This list matches the allowed tags in wp-admin/includes/theme-install.php.
     4433                        $themes_allowedtags = array('a' => array('href' => array(), 'title' => array(), 'target' => array()),
     4434                                'abbr' => array('title' => array()), 'acronym' => array('title' => array()),
     4435                                'code' => array(), 'pre' => array(), 'em' => array(), 'strong' => array(),
     4436                                'div' => array(), 'p' => array(), 'ul' => array(), 'ol' => array(), 'li' => array(),
     4437                                'h1' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'h5' => array(), 'h6' => array(),
     4438                                'img' => array('src' => array(), 'class' => array(), 'alt' => array())
     4439                        );
     4440
     4441                        // Prepare a list of installed themes to check against before the loop.
     4442                        $installed_themes = array();
     4443                        $wp_themes = wp_get_themes();
     4444                        foreach ( $wp_themes as $theme ) {
     4445                                $installed_themes[] = $theme->get_stylesheet();
     4446                        }
     4447                        $update_php = network_admin_url( 'update.php?action=install-theme' );
     4448
     4449                        // Set up properties for themes available on WordPress.org.
     4450                        foreach ( $themes->themes as &$theme ) {
     4451                                $theme->install_url = add_query_arg( array(
     4452                                        'theme'    => $theme->slug,
     4453                                        '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
     4454                                ), $update_php );
     4455
     4456                                $theme->name        = wp_kses( $theme->name, $themes_allowedtags );
     4457                                $theme->author      = wp_kses( $theme->author, $themes_allowedtags );
     4458                                $theme->version     = wp_kses( $theme->version, $themes_allowedtags );
     4459                                $theme->description = wp_kses( $theme->description, $themes_allowedtags );
     4460                                $theme->tags        = implode( ', ', $theme->tags );
     4461                                $theme->stars       = wp_star_rating( array( 'rating' => $theme->rating, 'type' => 'percent', 'number' => $theme->num_ratings, 'echo' => false ) );
     4462                                $theme->num_ratings = number_format_i18n( $theme->num_ratings );
     4463                                $theme->preview_url = set_url_scheme( $theme->preview_url );
     4464
     4465                                // Handle themes that are already installed as installed themes.
     4466                                if ( in_array( $theme->slug, $installed_themes, true ) ) {
     4467                                        $theme->type = 'installed';
     4468                                } else {
     4469                                        $theme->type = $_POST['theme_action'];
     4470                                }
     4471
     4472                                // Set active based on customized theme.
     4473                                if ( $_POST['customized_theme'] === $theme->slug ) {
     4474                                        $theme->active = true;
     4475                                } else {
     4476                                        $theme->active = false;
     4477                                }
     4478
     4479                                // Map available theme properties to installed theme properties.
     4480                                $theme->id           = $theme->slug;
     4481                                $theme->screenshot   = array( $theme->screenshot_url );
     4482                                $theme->authorAndUri = $theme->author;
     4483                                $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.
     4484                                unset( $theme->slug );
     4485                                unset( $theme->screenshot_url );
     4486                                unset( $theme->author );
     4487                        } // End foreach().
     4488                } // End if().
     4489                wp_send_json_success( $themes );
     4490        }
     4491
     4492
     4493        /**
    42954494         * Callback for validating the header_textcolor value.
    42964495         *
    42974496         * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
  • src/wp-includes/customize/class-wp-customize-theme-control.php

     
    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>
     
    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>
  • src/wp-includes/customize/class-wp-customize-themes-panel.php

     
     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="filter-themes-count">
     71                        <span class="themes-displayed"><?php
     72                                /* translators: %s: number of themes displayed; plural forms cannot be accommodated here so assume plurality or translate as "Themes: %s" */
     73                                echo sprintf( __( 'Displaying %s themes' ), '<span class="theme-count">0</span>' );
     74                        ?></span>
     75                        <button type="button" class="button button-primary see-themes"><?php
     76                                /* translators: %s: number of themes displayed; plural forms cannot be accommodated here so assume plurality or omit the count and translate as "Show themes" */
     77                                echo sprintf( __( 'Show %s themes' ), '<span class="theme-count">0</span>' );
     78                        ?></button>
     79                        <button type="button" class="button button-primary filter-themes"><?php _e( 'Filter themes' ); ?></button>
     80                </li>
     81                <li class="panel-meta customize-info accordion-section <# if ( ! data.description ) { #> cannot-expand<# } #>">
     82                        <button class="customize-panel-back" tabindex="-1"><span class="screen-reader-text"><?php _e( 'Back' ); ?></span></button>
     83                        <div class="accordion-section-title">
     84                                <span class="preview-notice"><?php
     85                                        /* translators: %s: themes panel title in the Customizer */
     86                                        echo sprintf( __( 'You are browsing %s' ), '<strong class="panel-title">' . __( 'Themes' ) . '</strong>' ); // Separate strings for consistency with other panels.
     87                                ?></span>
     88                                <?php if ( current_user_can( 'install_themes' ) && ! is_multisite() ) : ?>
     89                                        <# if ( data.description ) { #>
     90                                                <button class="customize-help-toggle dashicons dashicons-editor-help" tabindex="0" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
     91                                        <# } #>
     92                                <?php endif; ?>
     93                        </div>
     94                        <?php if ( current_user_can( 'install_themes' ) && ! is_multisite() ) : ?>
     95                                <# if ( data.description ) { #>
     96                                        <div class="description customize-panel-description">
     97                                                {{{ data.description }}}
     98                                        </div>
     99                                <# } #>
     100                        <?php endif; ?>
     101                </li>
     102                <li id="customize-themes-loading-container">
     103                        <span class="customize-loading-text-installing-theme"><?php _e( 'Downloading your new theme&hellip;' ); ?></span>
     104                        <span class="customize-loading-text"><?php _e( 'Setting up your live preview. This may take a bit.' ); ?></span>
     105                </li><?php // Used as a full-screen overlay transition after clicking to preview a theme. ?>
     106                <li class="customize-themes-full-container-container">
     107                        <ul class="customize-themes-full-container">
     108                                <li class="customize-themes-notifications"></li>
     109                        </ul>
     110                </li>
     111                <?php
     112        }
     113}
  • src/wp-includes/customize/class-wp-customize-themes-section.php

     
    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         *
    24          * @since 4.2.0
     23         * @since 4.9.0
    2524         * @var string
    2625         */
    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, latest, search, 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        public $action = '';
     37
     38        /**
     39         * Optional text to display before the theme section heading.
     40         *
     41         * @since 4.9.0
     42         * @var string
     43         */
     44        public $text_before = '';
     45
     46        /**
     47         * Get section parameters for JS.
     48         *
     49         * @since 4.9.0
     50         * @return array Exported parameters.
     51         */
     52        public function json() {
     53                $exported = parent::json();
     54                $exported['action'] = $this->action;
     55                $exported['text_before'] = $this->text_before;
     56
     57                return $exported;
     58        }
     59
     60        /**
     61         * Render a themes section as a JS template.
     62         *
     63         * The template is only rendered by PHP once, so all actions are prepared at once on the server side.
     64         *
     65         * @since 4.9.0
     66         */
     67        protected function render_template() {
    3668                ?>
    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;
     69                <li id="accordion-section-{{ data.id }}" class="theme-section">
     70                        <# if ( '' !== data.text_before ) { #>
     71                                <p class="customize-themes-text-before">{{ data.text_before }}</p>
     72                        <# } #>
     73                        <# if ( 'search' === data.action ) { #>
     74                                <div class="search-form customize-themes-section-title themes-section-search_themes">
     75                                        <label class="screen-reader-text" for="wp-filter-search-input">{{ data.title }}</label>
     76                                        <input placeholder="{{ data.title }}" type="text" aria-describedby="live-search-desc" id="wp-filter-search-input" class="wp-filter-search">
     77                                        <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span>
     78                                </div>
     79                        <# } else { #>
     80                                <# if ( 'favorites' === data.action || 'feature_filter' === data.action ) {
     81                                        var attr = ' aria-expanded="false"';
    4282                                } else {
    43                                         echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title;
    44                                 }
    45                                 ?>
    46 
    47                                 <?php if ( count( $this->controls ) > 0 ) : ?>
    48                                         <button type="button" class="button change-theme" tabindex="0"><?php _ex( 'Change', 'theme' ); ?></button>
    49                                 <?php endif; ?>
    50                         </h3>
    51                         <div class="customize-themes-panel control-panel-content themes-php">
    52                                 <h3 class="accordion-section-title customize-section-title">
    53                                         <button class="customize-section-back" tabindex="0" type="button"><span class="screen-reader-text"><?php _e( 'Back' ); ?></span></button>
    54                                         <span class="customize-action"><?php _e( 'Customizing' ); ?></span>
    55                                         <?php _e( 'Themes' ); ?>
    56                                         <span class="title-count theme-count"><?php echo count( $this->controls ) + 1 /* Active theme */; ?></span>
    57                                 </h3>
    58                                 <h3 class="accordion-section-title customize-section-title">
     83                                        var attr = '';
     84                                } #>
     85                                <button type="button" class="customize-themes-section-title themes-section-{{ data.id }}"{{{ attr }}}>{{ data.title }}</button>
     86                        <# } #>
     87                        <?php if ( ! current_user_can( 'install_themes' ) || is_multisite() ) : ?>
     88                                <# if ( 'installed' === data.action ) { #>
     89                                        <p class="themes-filter-container">
     90                                                <label for="themes-filter">
     91                                                        <span class="screen-reader-text"><?php _e( 'Search installed themes&hellip;' ); ?></span>
     92                                                        <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search installed themes&hellip;' ); ?>" />
     93                                                </label>
     94                                        </p>
     95                                <# } #>
     96                        <?php endif; ?>
     97                        <# if ( 'favorites' === data.action ) { #>
     98                                <div class="favorites-form filter-details">
     99                                        <p class="install-help"><?php _e( 'If you have marked themes as favorites on WordPress.org, you can browse them here.' ); ?></p>
     100                                        <p>
     101                                                <label for="wporg-username-input"><?php _e( 'Your WordPress.org username:' ); ?></label>
     102                                                <input type="search" id="wporg-username-input" value="">
     103                                                <button type="button" class="button button-secondary favorites-form-submit"><?php _e( 'Get Favorites' ); ?></button>
     104                                        </p>
     105                                </div>
     106                        <# } else if ( 'feature_filter' === data.action ) { #>
     107                                <div class="filter-drawer filter-details">
    59108                                        <?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;
     109                                        $feature_list = get_theme_feature_list();
     110                                        foreach ( $feature_list as $feature_name => $features ) {
     111                                                echo '<fieldset class="filter-group">';
     112                                                $feature_name = esc_html( $feature_name );
     113                                                echo '<legend><button type="button" class="button-link" aria-expanded="false">' . $feature_name . '</button></legend>';
     114                                                echo '<div class="filter-group-feature">';
     115                                                foreach ( $features as $feature => $feature_name ) {
     116                                                        $feature = esc_attr( $feature );
     117                                                        echo '<input type="checkbox" id="filter-id-' . $feature . '" value="' . $feature . '" /> ';
     118                                                        echo '<label for="filter-id-' . $feature . '">' . $feature_name . '</label><br>';
     119                                                }
     120                                                echo '</div>';
     121                                                echo '</fieldset>';
    64122                                        }
    65123                                        ?>
    66                                         <button type="button" class="button customize-theme"><?php _e( 'Customize' ); ?></button>
    67                                 </h3>
    68 
     124                                </div>
     125                        <# } #>
     126                        <div class="customize-themes-section themes-section-{{ data.id }} control-section-content themes-php">
    69127                                <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
    70 
    71                                 <div id="customize-container"></div>
    72                                 <?php if ( count( $this->controls ) > 4 ) : ?>
    73                                         <p><label for="themes-filter">
    74                                                 <span class="screen-reader-text"><?php _e( 'Search installed themes&hellip;' ); ?></span>
    75                                                 <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search installed themes&hellip;' ); ?>" />
    76                                         </label></p>
    77                                 <?php endif; ?>
    78128                                <div class="theme-browser rendered">
    79                                         <ul class="themes accordion-section-content">
    80                                         </ul>
     129                                        <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>
     130                                        <ul class="themes">
     131                                        </ul>
     132                                        <p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p>
     133                                        <p class="spinner"></p>
    81134                                </div>
    82135                        </div>
    83136                </li>
  • tests/phpunit/tests/customize/manager.php

     
    23512351                $data = json_decode( $json, true );
    23522352                $this->assertNotEmpty( $data );
    23532353
    2354                 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'customCss', 'changeset', 'timeouts' ), array_keys( $data ) );
     2354                $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'customCss', 'changeset', 'timeouts', 'l10n' ), array_keys( $data ) );
    23552355                $this->assertEquals( $autofocus, $data['autofocus'] );
    23562356                $this->assertArrayHasKey( 'save', $data['nonce'] );
    23572357                $this->assertArrayHasKey( 'preview', $data['nonce'] );