WordPress.org

Make WordPress Core

Ticket #37661: 37661.12.diff

File 37661.12.diff, 106.9 KB (added by celloexpressions, 21 months ago)

Current version from GitHub for records.

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

    diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css
    index 6c81fa8247..32452e7df2 100644
    a b body { 
    203203                    .15s border-color ease-in-out;
    204204}
    205205
    206 #customize-controls #customize-theme-controls .customize-themes-panel .accordion-section-title {
    207         color: #555;
    208         background-color: #fff;
    209         border-left: 4px solid #fff;
    210 }
    211 
    212206#customize-theme-controls .accordion-section-title:after {
    213207        content: "\f345";
    214208        color: #a0a5aa;
    body { 
    319313}
    320314
    321315#customize-theme-controls .customize-pane-child.open,
    322 #customize-theme-controls .customize-pane-child.current-panel,
    323 #customize-theme-controls .customize-themes-panel.customize-pane-child.current-panel {
     316#customize-theme-controls .customize-pane-child.current-panel {
    324317        -webkit-transform: none;
    325318        transform: none;
    326319}
    327320
    328 #customize-theme-controls .customize-themes-panel.customize-pane-child,
    329321.section-open #customize-theme-controls .customize-pane-parent,
    330322.in-sub-panel #customize-theme-controls .customize-pane-parent,
    331323.section-open #customize-info,
    332324.in-sub-panel #customize-info,
    333 .in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel,
    334 .in-themes-panel #customize-theme-controls .customize-pane-parent,
    335 .in-themes-panel #customize-info {
     325.in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel {
    336326        visibility: hidden;
    337327        height: 0;
    338328        overflow: hidden;
    body { 
    342332
    343333.section-open #customize-theme-controls .customize-pane-parent.busy,
    344334.in-sub-panel #customize-theme-controls .customize-pane-parent.busy,
    345 .in-themes-panel #customize-theme-controls .customize-pane-parent.busy,
    346335.section-open #customize-info.busy,
    347336.in-sub-panel #customize-info.busy,
    348 .in-themes-panel #customize-info.busy,
    349337.busy.section-open.in-sub-panel #customize-theme-controls .customize-pane-child.current-panel,
    350338#customize-theme-controls .customize-pane-child.open,
    351339#customize-theme-controls .customize-pane-child.current-panel,
    body { 
    355343        overflow: auto;
    356344}
    357345
    358 .in-themes-panel #customize-theme-controls .customize-pane-parent,
    359 .in-themes-panel #customize-info {
    360         -webkit-transform: translateX(100%);
    361         transform: translateX(100%);
    362 }
    363 
    364346#customize-theme-controls .customize-pane-child.accordion-section-content,
    365347#customize-theme-controls .customize-pane-child.accordion-sub-container {
    366348        display: block;
    p.customize-section-description { 
    12561238        100% { opacity: 1; }
    12571239}
    12581240
    1259 /* #customize-container is reused from customize-loader.js, hence the naming. */
    1260 .wp-customizer .customize-loading #customize-container {
     1241.wp-customizer .customize-loading #customize-themes-loading-container {
     1242        display: block;
     1243        -webkit-animation: customize-reload .5s; /* Can't use `transition` because `display` changes here. */
     1244        animation: customize-reload .5s;
     1245}
     1246
     1247.customize-loading #customize-themes-loading-container span {
     1248    clear: both;
     1249    color: #191e23;
     1250    font-size: 18px;
     1251    font-style: normal;
     1252    margin: 0;
     1253    padding: 2em 0;
     1254    text-align: center;
     1255        width: 100%;
    12611256        display: block;
    1262         -webkit-animation: customize-reload .75s; /* Can't use `transition` because `display` changes here. */
    1263         animation: customize-reload .75s;
     1257    top: 50%;
     1258    position: relative;
     1259}
     1260
     1261.customize-loading #customize-themes-loading-container .customize-loading-text {
     1262        display: none;
     1263}
     1264
     1265#customize-theme-controls .control-panel-themes {
     1266        border-bottom: none;
    12641267}
    12651268
    1266 #customize-theme-controls .control-section-themes .accordion-section-title:hover, /* Not a focusable element. */
    1267 #customize-theme-controls .control-section-themes .accordion-section-title {
     1269#customize-theme-controls .control-panel-themes > .accordion-section-title:hover, /* Not a focusable element. */
     1270#customize-theme-controls .control-panel-themes > .accordion-section-title {
    12681271        cursor: default;
    12691272        background: #fff;
    12701273        color: #555d66;
    12711274        border-top: 1px solid #ddd;
    12721275        border-bottom: 1px solid #ddd;
    12731276        border-left: none;
    1274         margin-top: 0;
    1275 }
    1276 #customize-theme-controls .control-section-themes .customize-section-back {
    1277         position: absolute;
    1278         right: 0;
    1279         top: 0;
    1280         height: 80px;
    1281         border-left: 1px solid #ddd;
    1282         border-right: 4px solid #fff;
    1283 }
    1284 #customize-theme-controls .control-section-themes .customize-section-back:before {
    1285         content: "\f345";
    1286 }
    1287 #customize-theme-controls .control-section-themes .customize-section-back:hover,
    1288 #customize-theme-controls .control-section-themes .customize-section-back:focus {
    1289         border-right-color: #0073aa;
     1277        border-right: none;
     1278        margin: 0 0 15px 0;
     1279        padding-right: 100px; /* Space for the button */
    12901280}
    12911281
    12921282#customize-theme-controls .control-section-themes .customize-themes-panel .accordion-section-title:first-child:hover, /* Not a focusable element. */
    p.customize-section-description { 
    12941284        border-top: 0;
    12951285}
    12961286
    1297 #customize-theme-controls .control-section-themes > .accordion-section-title:hover, /* Not a focusable element. */
    1298 #customize-theme-controls .control-section-themes > .accordion-section-title {
    1299         margin: 0 0 15px;
    1300 }
    1301 
    1302 #customize-controls .customize-themes-panel .accordion-section-title:hover,
    1303 #customize-controls .customize-themes-panel .accordion-section-title {
    1304         margin: 15px -8px;
    1305 }
    1306 
    1307 #customize-controls .control-section-themes .accordion-section-title,
    1308 #customize-controls .customize-themes-panel .accordion-section-title {
    1309         padding-right: 100px; /* Space for the button */
    1310 }
    1311 
    1312 #customize-controls .control-section-themes .accordion-section-title span.customize-action,
     1287.control-panel-themes .accordion-section-title span.customize-action,
    13131288#customize-controls .customize-section-title span.customize-action {
    13141289        font-size: 13px;
    13151290        display: block;
    13161291        font-weight: 400;
    13171292}
    13181293
    1319 #customize-controls .control-section-themes .accordion-section-title .change-theme,
    1320 #customize-controls .customize-themes-panel .accordion-section-title .customize-theme {
     1294#customize-theme-controls .control-panel-themes .accordion-section-title .change-theme {
    13211295        position: absolute;
    13221296        right: 10px;
    13231297        top: 50%;
    p.customize-section-description { 
    13251299        font-weight: 400;
    13261300}
    13271301
    1328 #customize-controls .control-section-themes .accordion-section-title:before {
     1302#customize-theme-controls .control-panel-themes > .accordion-section-title:after {
    13291303        display: none;
    13301304}
    13311305
    1332 #customize-controls .customize-themes-panel {
    1333         padding: 0 8px;
    1334         background: #f1f1f1;
     1306.control-panel-themes .customize-themes-full-container {
     1307        position: fixed;
     1308        top: 0;
     1309        left: 0;
     1310        -webkit-transition: .18s left ease-in-out;
     1311        transition: .18s left ease-in-out;
     1312        margin: 46px 0 0 300px;
     1313        padding: 25px 0;
     1314        overflow-y: scroll;
     1315        width: calc(100% - 300px);
     1316        height: calc(100% - 96px);
     1317        background: #eee;
     1318        z-index: 20;
     1319}
     1320
     1321/* Animations for opening the themes panel */
     1322#customize-header-actions .save,
     1323#customize-header-actions .spinner,
     1324#customize-header-actions .customize-controls-preview-toggle {
     1325        position: relative;
     1326        top: 0;
     1327        -webkit-transition: .18s top ease-in-out;
     1328        transition: .18s top ease-in-out;
     1329}
     1330
     1331#customize-footer-actions,
     1332#customize-footer-actions .collapse-sidebar {
     1333        bottom: 0;
     1334        -webkit-transition: .18s bottom ease-in-out;
     1335        transition: .18s bottom ease-in-out;
     1336}
     1337
     1338.in-themes-panel:not(.animating) #customize-header-actions .save,
     1339.in-themes-panel:not(.animating) #customize-header-actions .spinner,
     1340.in-themes-panel:not(.animating) #customize-header-actions .customize-controls-preview-toggle,
     1341.in-themes-panel:not(.animating) #customize-preview,
     1342.in-themes-panel:not(.animating) #customize-footer-actions {
     1343        visibility: hidden;
     1344}
     1345
     1346.wp-full-overlay.in-themes-panel {
     1347        background: #eee; /* Prevents a black flash when fading in the panel */
     1348}
     1349
     1350.in-themes-panel #customize-header-actions .save,
     1351.in-themes-panel #customize-header-actions .spinner,
     1352.in-themes-panel #customize-header-actions .customize-controls-preview-toggle {
     1353        top: -45px;
     1354}
     1355
     1356.in-themes-panel #customize-footer-actions,
     1357.in-themes-panel #customize-footer-actions .collapse-sidebar {
     1358        bottom: -45px;
     1359}
     1360
     1361/* Don't show the theme count while the panel opens, as it's in the wrong place during the animation */
     1362.in-themes-panel.animating .control-panel-themes .filter-themes-count {
     1363        display: none;
     1364}
     1365
     1366.in-themes-panel.wp-full-overlay .wp-full-overlay-sidebar-content {
     1367        bottom: 0;
     1368}
     1369
     1370.themes-filter-bar .feature-filter-toggle {
     1371        float: right;
     1372        margin: 3px 0 3px 25px;
     1373}
     1374
     1375.themes-filter-bar .feature-filter-toggle:before {
     1376    content: "\f111";
     1377    margin: 0 5px 0 0;
     1378    font: normal 16px/1 dashicons;
     1379    vertical-align: text-bottom;
     1380    -webkit-font-smoothing: antialiased;
     1381    -moz-osx-font-smoothing: grayscale;
     1382}
     1383
     1384.themes-filter-bar .feature-filter-toggle.open {
     1385        background: #eee;
     1386        border-color: #999;
     1387        box-shadow: inset 0 2px 5px -3px rgba( 0, 0, 0, 0.5 );
     1388        -webkit-transform: translateY(1px);
     1389        transform: translateY(1px);
     1390}
     1391
     1392.themes-filter-bar .feature-filter-toggle .filter-count-filters {
     1393        display: none;
     1394}
     1395
     1396.themes-filter-bar .filter-drawer {
    13351397        box-sizing: border-box;
     1398        width: 100%;
     1399        position: absolute;
     1400        top: 46px;
     1401        left: 0;
     1402        padding: 25px 0 25px 25px;
     1403        border-top: 0;
     1404        margin: 0;
     1405        background: #eee;
     1406        border-bottom: 1px solid #ddd;
    13361407}
    13371408
    1338 #customize-controls .customize-themes-panel .accordion-section-title:first-child {
    1339         margin-top: 0;
     1409.themes-filter-bar .filter-group {
     1410        margin: 0 25px 0 0;
     1411        width: calc( (100% - 75px) / 3);
     1412        min-width: 200px;
     1413        max-width: 320px;
     1414}
     1415
     1416/* Adds a delay before fading in to avoid it "jumping" */
     1417@-webkit-keyframes themes-fade-in {
     1418        0% {
     1419                opacity: 0;
     1420        }
     1421        50% {
     1422                opacity: 0;
     1423        }
     1424        100% {
     1425                opacity: 1;
     1426        }
     1427}
     1428@keyframes themes-fade-in {
     1429        0% {
     1430                opacity: 0;
     1431        }
     1432        50% {
     1433                opacity: 0;
     1434        }
     1435        100% {
     1436                opacity: 1;
     1437        }
     1438}
     1439
     1440.control-panel-themes .customize-themes-full-container.animate {
     1441        -webkit-animation: .6s themes-fade-in 1;
     1442        animation: .6s themes-fade-in 1;
     1443}
     1444
     1445.in-themes-panel:not(.animating) .control-panel-themes .filter-themes-count {
     1446        -webkit-animation: .6s themes-fade-in 1;
     1447        animation: .6s themes-fade-in 1;
    13401448}
    13411449
    1342 #customize-controls .customize-themes-panel .accordion-section-title:nth-child(2) {
     1450.control-panel-themes .filter-themes-count {
     1451        position: relative;
     1452        float: right;
     1453        line-height: 34px;
     1454}
     1455
     1456.control-panel-themes .filter-themes-count .themes-displayed {
     1457        font-weight: 600;
     1458        color: #555d66;
     1459}
     1460
     1461.customize-themes-notifications {
     1462        margin: 0;
     1463}
     1464
     1465.control-panel-themes .customize-themes-notifications .notice {
     1466        margin: 0 0 25px 0;
     1467}
     1468
     1469.customize-themes-full-container .customize-themes-section {
     1470        display: none !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1471        overflow: hidden;
     1472}
     1473
     1474.customize-themes-full-container .customize-themes-section.current-section {
     1475        display: list-item !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
     1476}
     1477
     1478.control-section .customize-section-text-before {
     1479        padding: 0 0 8px 15px;
     1480        margin: 15px 0 0 0;
     1481        line-height: 16px;
     1482        border-bottom: 1px solid #ddd;
     1483        color: #555d66;
     1484}
     1485
     1486.control-panel-themes .customize-themes-section-title {
     1487        width: 100%;
     1488        background: #fff;
     1489        -webkit-box-shadow: none;
     1490        box-shadow: none;
     1491        outline: none;
     1492        border-top: none;
     1493        border-bottom: 1px solid #ddd;
     1494        border-left: 4px solid #fff;
     1495        border-right: none;
     1496        cursor: pointer;
     1497        padding: 10px 15px;
     1498        position: relative;
     1499        text-align: left;
    13431500        font-size: 14px;
    13441501        font-weight: 600;
     1502        color: #555d66;
     1503        text-shadow: none;
    13451504}
    13461505
    1347 #customize-controls .customize-themes-panel > h2 {
    1348         padding: 15px 8px 0 8px;
     1506.control-panel-themes #accordion-section-installed_themes {
     1507        border-top: 1px solid #ddd;
    13491508}
    13501509
    1351 #customize-theme-controls .customize-themes-panel .accordion-section-content {
    1352         background: transparent;
     1510.control-panel-themes .theme-section {
     1511        margin: 0;
     1512        position: relative;
     1513}
     1514
     1515.control-panel-themes .customize-themes-section-title:focus,
     1516.control-panel-themes .customize-themes-section-title:hover {
     1517        border-left-color: #0073aa;
     1518        color: #0073aa;
     1519        background: #f5f5f5;
     1520}
     1521
     1522.customize-themes-section-title:not(.selected):after {
     1523        content: "";
    13531524        display: block;
     1525        position: absolute;
     1526        top: 9px;
     1527        right: 15px;
     1528        width: 18px;
     1529        height: 18px;
     1530        border-radius: 100%;
     1531        border: 1px solid #ccc;
     1532        background: #fff;
    13541533}
    13551534
    1356 .customize-control.customize-control-theme {
    1357         margin-bottom: 8px;
     1535.control-panel-themes .theme-section .customize-themes-section-title.selected:after {
     1536        content: "\f147";
     1537        font: 16px/1 dashicons;
     1538        box-sizing: border-box;
     1539        width: 20px;
     1540        height: 20px;
     1541        padding: 3px 3px 1px 1px; /* Re-align the icon to the smaller grid */
     1542        -webkit-border-radius: 100%;
     1543        border-radius: 100%;
     1544        position: absolute;
     1545        top: 9px;
     1546        right: 15px;
     1547        background: #0073aa;
     1548        color: #fff;
     1549}
     1550
     1551.control-panel-themes .customize-themes-section-title.selected {
     1552        color: #0073aa;
    13581553}
    13591554
    13601555#customize-theme-controls .themes.accordion-section-content {
    p.customize-section-description { 
    13641559        width: 100%;
    13651560}
    13661561
    1367 .wp-customizer .theme-browser .themes {
    1368         padding-bottom: 8px;
     1562.loading .customize-themes-section .spinner {
     1563        display: block;
     1564        visibility: visible;
     1565        position: relative;
     1566        clear: both;
     1567        width: 20px;
     1568        height: 20px;
     1569        left: -webkit-calc(50% - 10px);
     1570        left: calc(50% - 10px);
     1571        float: none;
     1572        margin-top: 50px;
    13691573}
    13701574
    1371 .wp-customizer .theme-browser .theme {
    1372         margin: 0;
     1575.customize-themes-section .no-themes,
     1576.customize-themes-section .no-themes-local {
     1577        display: none;
     1578}
     1579
     1580.themes-section-installed_themes .theme .notice-success {
     1581        display: none; /* Hide "installed" notice on installed themes tab. */
     1582}
     1583
     1584.control-panel-themes .theme-browser .theme .theme-actions .button-primary {
     1585        margin: 0 0 0 8px;
     1586}
     1587
     1588.customize-control-theme .theme {
    13731589        width: 100%;
     1590        margin: 0;
     1591        border: 1px solid #ddd;
     1592        background: #fff;
     1593}
     1594
     1595.customize-control-theme .theme .theme-name, .customize-control-theme .theme .theme-actions {
     1596        background: #fff;
     1597        border: none;
     1598}
     1599
     1600.customize-control.customize-control-theme { /* override most properties on .customize-control */
     1601        -webkit-box-sizing: border-box;
     1602        -moz-box-sizing: border-box;
     1603        box-sizing: border-box;
     1604        width: 25%;
     1605        max-width: 600px; /* Max. screenshot size / 2 */
     1606        margin: 0 25px 25px 0;
     1607        padding: 0;
     1608        clear: none;
     1609}
     1610
     1611/* 5 columns above 2100px */
     1612@media screen and (min-width: 2101px) {
     1613        .customize-control.customize-control-theme {
     1614                width: calc( ( 100% - 125px ) / 5 - 1px ); /* 1px offset accounts for browser rounding, typical all grids */
     1615        }
     1616}
     1617
     1618/* 4 columns up to 2100px */
     1619@media screen and (min-width: 1601px) and (max-width: 2100px) {
     1620        .customize-control.customize-control-theme {
     1621                width: calc( ( 100% - 100px ) / 4 - 1px );
     1622        }
     1623}
     1624
     1625/* 3 columns up to 1600px */
     1626@media screen and (min-width: 1201px) and (max-width: 1600px) {
     1627        .customize-control.customize-control-theme {
     1628                width: calc( ( 100% - 75px ) / 3 - 1px );
     1629        }
     1630}
     1631
     1632/* 2 columns up to 1200px */
     1633@media screen and (min-width: 851px) and (max-width: 1200px) {
     1634        .customize-control.customize-control-theme {
     1635                width: calc( ( 100% - 50px ) / 2 - 1px );
     1636
     1637        }
     1638}
     1639
     1640/* 1 column up to 850 px */
     1641@media screen and (max-width: 850px) {
     1642        .customize-control.customize-control-theme {
     1643                width: 100%;
     1644        }
     1645}
     1646
     1647.wp-customizer .theme-browser .themes {
     1648        padding: 0 0 25px 25px;
     1649        transition: .18s margin-top linear;
    13741650}
    13751651
    13761652.wp-customizer .theme-browser .theme .theme-actions {
    1377         -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
    13781653        opacity: 1;
    13791654}
    13801655
    p.customize-section-description { 
    13861661        font-size: 32px;
    13871662}
    13881663
    1389 .wp-customizer #themes-filter {
    1390         font-size: 16px;
    1391         font-weight: 300;
    1392         line-height: 1.5;
    1393         width: 100%;
     1664.customize-preview-header.themes-filter-bar {
     1665        position: fixed;
     1666        top: 0;
     1667        left: 300px;
     1668        width: calc(100% - 300px);
     1669        height: 46px;
     1670        background: #eee;
     1671        z-index: 10;
     1672        padding: 6px 25px;
     1673        box-sizing: border-box;
     1674        border-bottom: 1px solid #ddd;
     1675}
     1676
     1677.themes-filter-bar .themes-filter-container {
     1678        margin: 0;
     1679        padding: 0;
     1680}
     1681
     1682.themes-filter-bar .wp-filter-search {
     1683        line-height: 25px;
     1684        padding: 3px 5px;
     1685        max-width: 100%;
     1686        width: 40%;
     1687        min-width: 300px;
     1688        position: absolute;
     1689        top: 6px;
     1690        left: 25px;
    13941691}
    13951692
    1396 .control-section-themes .accordion-section-title:after,
    1397 .customize-themes-panel .accordion-section-title:after {
     1693/* Unstick the filter bar on short windows/screens. This breakpoint is based on the
     1694   current length of .org feature filters assuming translations do not wrap lines. */
     1695@media screen and (max-height:540px), screen and (max-width:1018px) {
     1696        .customize-preview-header.themes-filter-bar {
     1697                position: relative;
     1698                left: 0;
     1699                width: 100%;
     1700                margin: 0 0 25px 0;
     1701        }
     1702        .wp-customizer .theme-browser .themes {
     1703                padding: 0 0 25px 25px;
     1704                overflow: hidden;
     1705        }
     1706
     1707        .control-panel-themes .customize-themes-full-container {
     1708                margin-top: 0;
     1709                padding: 0;
     1710                height: 100%;
     1711                width: calc(100% - 300px);
     1712        }
     1713}
     1714
     1715@media screen and (max-width:1018px) {
     1716        .themes-filter-bar .filter-group {
     1717                width: calc( (100% - 50px) / 2);
     1718        }
     1719}
     1720
     1721@media screen and (max-width:900px) {
     1722        .customize-preview-header.themes-filter-bar {
     1723                height: 86px;
     1724                padding-top: 46px;
     1725        }
     1726
     1727        .themes-filter-bar .wp-filter-search {
     1728                width: calc(100% - 50px);
     1729                margin: 0;
     1730                min-width: 200px;
     1731        }
     1732
     1733        .themes-filter-bar .filter-drawer {
     1734                top: 86px;
     1735        }
     1736
     1737        .control-panel-themes .filter-themes-count {
     1738                float: left;
     1739        }
     1740}
     1741
     1742@media screen and (max-width:792px) {
     1743        .themes-filter-bar .filter-group {
     1744                width: calc( 100% - 25px);
     1745        }
     1746}
     1747
     1748.control-panel-themes .customize-themes-mobile-back {
    13981749        display: none;
    13991750}
    14001751
    1401 .customize-themes-panel.control-panel-content {
    1402         border-top: 1px solid #ddd;
     1752/* Mobile - toggle between themes and filters */
     1753@media screen and (max-width:600px) {
     1754
     1755        .wp-full-overlay.showing-themes .control-panel-themes .filter-themes-count .filter-themes {
     1756                display: block;
     1757                float: right;
     1758        }
     1759
     1760        .control-panel-themes .customize-themes-full-container {
     1761                width: 100%;
     1762                margin: 0;
     1763                top: 46px;
     1764                height: calc(100% - 46px);
     1765                z-index: 1;
     1766                display: none;
     1767        }
     1768
     1769        .showing-themes .control-panel-themes .customize-themes-full-container {
     1770                display: block;
     1771        }
     1772
     1773        .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back {
     1774                display: block;
     1775                position: fixed;
     1776                top: 0;
     1777                left: 0;
     1778                background: #eee;
     1779                color: #444;
     1780                border-radius: 0;
     1781                box-shadow: none;
     1782                border: none;
     1783                height: 46px;
     1784                width: 100%;
     1785                z-index: 10;
     1786                text-align: left;
     1787                text-shadow: none;
     1788                border-bottom: 1px solid #ddd;
     1789                border-left: 4px solid transparent;
     1790                margin: 0;
     1791                padding: 0;
     1792                font-size: 0;
     1793                overflow: hidden;
     1794        }
     1795
     1796        .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:before {
     1797                left: 0;
     1798                top: 0;
     1799                height: 42px;
     1800                width: 26px;
     1801                display: block;
     1802                line-height: 46px;
     1803                padding: 0 8px 0 8px;
     1804                border-right: 1px solid #ddd;
     1805        }
     1806
     1807        .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:hover,
     1808        .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:focus {
     1809            color: #0073aa;
     1810            background: #f3f3f5;
     1811            border-left-color: #0073aa;
     1812            outline: none;
     1813            box-shadow: none;
     1814        }
     1815
     1816        .showing-themes #customize-header-actions {
     1817                display: none;
     1818        }
    14031819}
    14041820
    14051821/* Details View */
    p.customize-section-description { 
    14161832        z-index: 109;
    14171833}
    14181834
     1835/* Avoid a z-index war by resetting elements that should be under the overlay.
     1836   This is likely required because of the way that sections and panels are positioned. */
     1837.wp-customizer.modal-open #customize-header-actions,
     1838.wp-customizer.modal-open .control-panel-themes .filter-themes-count,
     1839.wp-customizer.modal-open .control-panel-themes .customize-themes-section-title.selected:after {
     1840        z-index: -1;
     1841}
     1842
    14191843.wp-customizer .theme-overlay .theme-backdrop {
    14201844        background: rgba( 238, 238, 238, 0.75 );
    14211845        position: fixed;
    14221846        z-index: 110;
    14231847}
    14241848
     1849.wp-customizer .theme-overlay .star-rating {
     1850        float: left;
     1851        margin-right: 8px;
     1852}
     1853
     1854.wp-customizer .theme-rating .num-ratings {
     1855        line-height: 20px;
     1856}
     1857
    14251858.wp-customizer .theme-overlay .theme-wrap {
    14261859        left: 90px;
    14271860        right: 90px;
    14281861        top: 45px;
    14291862        bottom: 45px;
    14301863        z-index: 120;
    1431         max-width: 1740px; /* To ensure that theme screenshots are not displayed larger than 880px wide. */
    14321864}
    14331865
    14341866.wp-customizer .theme-overlay .theme-actions {
    1435         text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */
     1867        text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */
     1868        padding: 10px 25px;
     1869        background: #eee;
    14361870}
    14371871
    1438 .ie8 .wp-customizer .theme-overlay .theme-header,
    1439 .ie8 .wp-customizer .theme-overlay .theme-about,
    1440 .ie8 .wp-customizer .theme-overlay .theme-actions {
    1441         position: static;
     1872.wp-customizer .theme-overlay .theme-actions .theme-install.preview {
     1873        margin-left: 8px;
     1874}
     1875
     1876.control-panel-themes .theme-actions .delete-theme {
     1877        left: 15px; /* these override themes.css on mobile */
     1878        right: auto;
     1879        bottom: auto;
     1880        position: absolute;
     1881}
     1882
     1883.modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content {
     1884        overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */
     1885}
     1886
     1887.wp-customizer .theme-header {
     1888        background: #eee;
     1889}
     1890
     1891.wp-customizer .theme-overlay .theme-header button,
     1892.wp-customizer .theme-overlay .theme-header .close:before {
     1893        color: #444;
     1894}
     1895
     1896.wp-customizer .theme-overlay .theme-header .close:focus,
     1897.wp-customizer .theme-overlay .theme-header .close:hover,
     1898.wp-customizer .theme-overlay .theme-header .right:focus,
     1899.wp-customizer .theme-overlay .theme-header .right:hover,
     1900.wp-customizer .theme-overlay .theme-header .left:focus,
     1901.wp-customizer .theme-overlay .theme-header .left:hover {
     1902        background: #fff;
     1903        border-bottom: 4px solid #0073aa;
     1904        color: #0073aa;
     1905}
     1906
     1907.wp-customizer .theme-overlay .theme-header button.disabled {
     1908        border-bottom: none;
     1909        background: transparent;
     1910        color: #ccc;
     1911}
     1912
     1913.wp-customizer .theme-overlay .theme-header .close:focus:before,
     1914.wp-customizer .theme-overlay .theme-header .close:hover:before {
     1915        color: #0073aa;
    14421916}
    14431917
    14441918/* Small Screens */
    body.cheatin { 
    14661940body.cheatin h1 {
    14671941        border-bottom: 1px solid #ddd;
    14681942        clear: both;
    1469         color: #666;
     1943        color: #555d66;
    14701944        font-size: 24px;
    14711945        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
    14721946        margin: 30px 0 0 0;
  • src/wp-admin/css/themes.css

    diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css
    index 6c4ce8716a..b6522952e6 100644
    a b body.folded .theme-browser ~ .theme-overlay .theme-wrap { 
    549549        float: left;
    550550        margin: 0 30px 0 0;
    551551        width: 55%;
    552         max-width: 880px;
     552        max-width: 1200px; /* Recommended theme screenshot width, set here to avoid stretching */
    553553        text-align: center;
    554554}
    555555
    body.folded .theme-browser ~ .theme-overlay .theme-wrap { 
    10491049        text-align: center;
    10501050}
    10511051
    1052 p.no-themes {
     1052p.no-themes,
     1053p.no-themes-local {
    10531054        clear: both;
    10541055        color: #666;
    10551056        font-size: 18px;
    body.full-overlay-active { 
    17051706        display: none;
    17061707}
    17071708
    1708 #customize-container {
     1709#customize-container,
     1710#customize-themes-loading-container {
    17091711        display: none;
    1710         background: #fff;
     1712        background: #eee;
    17111713        z-index: 500000;
    17121714        position: fixed;
    17131715        overflow: visible;
    body.full-overlay-active { 
    17201722
    17211723/* Make the Customizer and Theme installer overlays the only available content. */
    17221724#customize-container,
     1725#customize-themes-loading-container,
    17231726.theme-install-overlay {
    17241727        visibility: visible;
    17251728}
    body.full-overlay-active { 
    18241827
    18251828#customize-preview.wp-full-overlay-main:before,
    18261829.customize-loading #customize-container:before,
     1830.customize-loading #customize-themes-loading-container:before,
    18271831.theme-install-overlay .wp-full-overlay-main:before {
    18281832        content: "";
    18291833        display: block;
    body.full-overlay-active { 
    18611865
    18621866        #customize-preview.wp-full-overlay-main:before,
    18631867        .customize-loading #customize-container:before,
     1868        .customize-loading #customize-themes-loading-container:before,
    18641869        .theme-install-overlay .wp-full-overlay-main:before {
    18651870                background-image: url(../images/spinner-2x.gif);
    18661871        }
  • src/wp-admin/customize.php

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

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

    diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js
    index 17ce623f6c..57c1aae5cc 100644
    a b  
    11351135                                section = this,
    11361136                                container = $( '#customize-theme-controls' );
    11371137
    1138                         // Watch for changes to the panel state
     1138                        // Watch for changes to the panel state.
    11391139                        inject = function ( panelId ) {
    11401140                                var parentContainer;
    11411141                                if ( panelId ) {
    1142                                         // The panel has been supplied, so wait until the panel object is registered
     1142                                        // The panel has been supplied, so wait until the panel object is registered.
    11431143                                        api.panel( panelId, function ( panel ) {
    1144                                                 // The panel has been registered, wait for it to become ready/initialized
     1144                                                // The panel has been registered, wait for it to become ready/initialized.
    11451145                                                panel.deferred.embedded.done( function () {
    11461146                                                        parentContainer = panel.contentContainer;
    11471147                                                        if ( ! section.headContainer.parent().is( parentContainer ) ) {
     
    11661166                                }
    11671167                        };
    11681168                        section.panel.bind( inject );
    1169                         inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
     1169                        inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
    11701170                },
    11711171
    11721172                /**
     
    13391339        /**
    13401340         * wp.customize.ThemesSection
    13411341         *
    1342          * Custom section for themes that functions similarly to a backwards panel,
    1343          * and also handles the theme-details view rendering and navigation.
     1342         * Custom section for themes that loads themes by category, and also
     1343         * handles the theme-details view rendering and navigation.
    13441344         *
    13451345         * @constructor
    13461346         * @augments wp.customize.Section
     
    13521352                template: '',
    13531353                screenshotQueue: null,
    13541354                $window: $( window ),
     1355                loaded: 0,
     1356                loading: false,
     1357                fullyLoaded: false,
     1358                term: '',
     1359                tags: '',
     1360                nextTerm: '',
     1361                nextTags: '',
     1362                filtersHeight: 0,
     1363                headerContainer: $(),
    13551364
    13561365                /**
    1357                  * @since 4.2.0
     1366                 * Embed the section in the DOM when the themes panel is ready.
     1367                 *
     1368                 * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
     1369                 *
     1370                 * @since 4.9.0
    13581371                 */
    1359                 initialize: function () {
    1360                         this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
    1361                         return api.Section.prototype.initialize.apply( this, arguments );
     1372                embed: function() {
     1373                        var inject,
     1374                                section = this,
     1375                                container = $( '#customize-theme-controls' );
     1376
     1377                        // Watch for changes to the panel state
     1378                        inject = function( panelId ) {
     1379                                var parentContainer;
     1380                                api.panel( panelId, function( panel ) {
     1381
     1382                                        // The panel has been registered, wait for it to become ready/initialized
     1383                                        panel.deferred.embedded.done( function() {
     1384                                                parentContainer = panel.contentContainer;
     1385                                                if ( ! section.headContainer.parent().is( parentContainer ) ) {
     1386                                                        parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
     1387                                                }
     1388                                                if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
     1389                                                        container.append( section.contentContainer );
     1390                                                }
     1391                                                section.deferred.embedded.resolve();
     1392                                        });
     1393                                } );
     1394                        };
     1395                        section.panel.bind( inject );
     1396                        inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
    13621397                },
    13631398
    13641399                /**
    13651400                 * @since 4.2.0
    13661401                 */
    1367                 ready: function () {
     1402                ready: function() {
    13681403                        var section = this;
    13691404                        section.overlay = section.container.find( '.theme-overlay' );
    13701405                        section.template = wp.template( 'customize-themes-details-view' );
     
    13871422
    13881423                                // Pressing the escape key fires a theme:collapse event
    13891424                                if ( 27 === event.keyCode ) {
    1390                                         section.closeDetails();
     1425                                        if ( $( 'body' ).hasClass( 'modal-open' ) ) {
     1426
     1427                                                // Escape from the details modal.
     1428                                                section.closeDetails();
     1429                                        } else {
     1430
     1431                                                // Escape from the inifinite scroll list.
     1432                                                section.headerContainer.find( '.customize-themes-section-title' ).focus();
     1433                                        }
    13911434                                        event.stopPropagation(); // Prevent section from being collapsed.
    13921435                                }
    13931436                        });
    13941437
    1395                         _.bindAll( this, 'renderScreenshots' );
     1438                        _.bindAll( this, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
    13961439                },
    13971440
    13981441                /**
    13991442                 * Override Section.isContextuallyActive method.
    14001443                 *
    14011444                 * Ignore the active states' of the contained theme controls, and just
    1402                  * use the section's own active state instead. This ensures empty search
    1403                  * results for themes to cause the section to become inactive.
     1445                 * use the section's own active state instead. This prevents empty search
     1446                 * results for theme sections from causing the section to become inactive.
    14041447                 *
    14051448                 * @since 4.2.0
    14061449                 *
     
    14141457                 * @since 4.2.0
    14151458                 */
    14161459                attachEvents: function () {
    1417                         var section = this;
     1460                        var section = this, debounced;
    14181461
    14191462                        // Expand/Collapse accordion sections on click.
    14201463                        section.container.find( '.customize-section-back' ).on( 'click keydown', function( event ) {
     
    14251468                                section.collapse();
    14261469                        });
    14271470
    1428                         // Expand/Collapse section/panel.
    1429                         section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
    1430                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1431                                         return;
     1471                        section.headerContainer = $( '#accordion-section-' + section.id );
     1472
     1473                        // Expand section/panel. Only collapse when opening another section.
     1474                        section.headerContainer.on( 'click', '.customize-themes-section-title', function() {
     1475
     1476                                // Toggle accordion filters under section headers.
     1477                                if ( section.headerContainer.find( '.filter-details' ).length ) {
     1478                                        section.headerContainer.find( '.customize-themes-section-title' )
     1479                                                .toggleClass( 'details-open' )
     1480                                                .attr( 'aria-expanded', function( i, attr ) {
     1481                                                        return 'true' === attr ? 'false' : 'true';
     1482                                                });
     1483                                        section.headerContainer.find( '.filter-details' ).slideToggle( 180 );
    14321484                                }
    1433                                 event.preventDefault(); // Keep this AFTER the key filter above
    14341485
    1435                                 if ( section.expanded() ) {
    1436                                         section.collapse();
    1437                                 } else {
     1486                                // Open the section.
     1487                                if ( ! section.expanded() ) {
    14381488                                        section.expand();
    14391489                                }
    14401490                        });
    14411491
    1442                         // Theme navigation in details view.
    1443                         section.container.on( 'click keydown', '.left', function( event ) {
    1444                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1445                                         return;
    1446                                 }
     1492                        // Preview installed themes.
     1493                        section.container.on( 'click', '.theme-actions .preview-theme', function() {
     1494                                var themeId = $( this ).data( 'slug' );
    14471495
    1448                                 event.preventDefault(); // Keep this AFTER the key filter above
     1496                                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     1497                                api.panel( 'themes' ).loadThemePreview( themeId ).fail( function() {
     1498                                        $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     1499                                } );
     1500                        });
    14491501
     1502                        // Theme navigation in details view.
     1503                        section.container.on( 'click', '.left', function() {
    14501504                                section.previousTheme();
    14511505                        });
    14521506
    1453                         section.container.on( 'click keydown', '.right', function( event ) {
    1454                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1455                                         return;
    1456                                 }
    1457 
    1458                                 event.preventDefault(); // Keep this AFTER the key filter above
    1459 
     1507                        section.container.on( 'click', '.right', function() {
    14601508                                section.nextTheme();
    14611509                        });
    14621510
    1463                         section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
    1464                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
    1465                                         return;
    1466                                 }
    1467 
    1468                                 event.preventDefault(); // Keep this AFTER the key filter above
    1469 
     1511                        section.container.on( 'click', '.theme-backdrop, .close', function() {
    14701512                                section.closeDetails();
    14711513                        });
    14721514
    1473                         var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
    1474                         section.container.on( 'input', '#themes-filter', function( event ) {
    1475                                 var count,
    1476                                         term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
    1477                                         controls = section.controls();
     1515                        // Filter-search all theme objects loaded in the section.
     1516                        section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
     1517                                        section.filterSearch( event.currentTarget );
     1518                        });
     1519
     1520                        // Event listeners for remote wporg queries with user-entered terms.
     1521                        if ( 'wporg' === section.params.action ) {
    14781522
    1479                                 _.each( controls, function( control ) {
    1480                                         control.filter( term );
     1523                                // Search terms.
     1524                                debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
     1525                                section.contentContainer.on( 'input', '#wp-filter-search-input', function() {
     1526                                        debounced( section );
     1527                                        if ( ! section.expanded() ) {
     1528                                                section.expand();
     1529                                        }
     1530                                        section.checkTerm( section );
    14811531                                });
    14821532
    1483                                 renderScreenshots();
     1533                                // Feature filters.
     1534                                section.contentContainer.on( 'click', '.filter-group input', function() {
     1535                                        section.filtersChecked();
     1536                                        section.checkTerm( section );
     1537                                });
     1538
     1539                                // Toggle feature filter sections.
     1540                                section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
     1541                                        $( e.currentTarget )
     1542                                                .toggleClass( 'open' )
     1543                                                .attr( 'aria-expanded', function( i, attr ) {
     1544                                                        return 'true' == attr ? 'false' : 'true';
     1545                                                })
     1546                                                .next( '.filter-drawer' ).slideToggle( 180, 'linear', function() {
     1547                                                        if ( 0 === section.filtersHeight ) {
     1548                                                                section.filtersHeight = $( this ).height();
     1549
     1550                                                                // First time, so it's opened.
     1551                                                                section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
     1552                                                        }
     1553                                                });
     1554                                        if ( $( e.currentTarget ).hasClass( 'open' ) ) {
     1555                                                section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
     1556                                        } else {
     1557                                                section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
     1558                                        }
     1559                                });
     1560                        }
    14841561
    1485                                 // Update theme count.
    1486                                 count = section.container.find( 'li.customize-control:visible' ).length;
    1487                                 section.container.find( '.theme-count' ).text( count );
     1562                        // Setup section cross-linking.
     1563                        section.contentContainer.on( 'click', '.no-themes-local .search-dotorg-themes', function() {
     1564                                api.section( 'wporg_themes' ).focus();
    14881565                        });
    14891566
    1490                         // Pre-load the first 3 theme screenshots.
     1567                        // Move section controls to the themes area.
    14911568                        api.bind( 'ready', function () {
    1492                                 _.each( section.controls().slice( 0, 3 ), function ( control ) {
    1493                                         var img, src = control.params.theme.screenshot[0];
    1494                                         if ( src ) {
    1495                                                 img = new Image();
    1496                                                 img.src = src;
    1497                                         }
    1498                                 });
     1569                                section.contentContainer = section.container.find( '.customize-themes-section' );
     1570                                section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
     1571                                section.container.add( section.headerContainer );
    14991572                        });
    15001573                },
    15011574
     
    15071580                 * @param {Boolean}  expanded
    15081581                 * @param {Object}   args
    15091582                 * @param {Boolean}  args.unchanged
    1510                  * @param {Callback} args.completeCallback
     1583                 * @param {Function} args.completeCallback
    15111584                 */
    15121585                onChangeExpanded: function ( expanded, args ) {
    15131586
     1587                        // Note: there is a second argument 'args' passed
     1588                        var section = this,
     1589                                container = section.contentContainer.closest( '.customize-themes-full-container' );
     1590
    15141591                        // Immediately call the complete callback if there were no changes
    15151592                        if ( args.unchanged ) {
    15161593                                if ( args.completeCallback ) {
     
    15191596                                return;
    15201597                        }
    15211598
    1522                         // Note: there is a second argument 'args' passed
    1523                         var panel = this,
    1524                                 section = panel.contentContainer,
    1525                                 overlay = section.closest( '.wp-full-overlay' ),
    1526                                 container = section.closest( '.wp-full-overlay-sidebar-content' ),
    1527                                 customizeBtn = section.find( '.customize-theme' ),
    1528                                 changeBtn = panel.headContainer.find( '.change-theme' );
     1599                        if ( expanded ) {
     1600
     1601                                // Try to load controls if none are loaded yet.
     1602                                if ( 0 === section.loaded ) {
     1603                                        section.loadControls();
     1604                                }
    15291605
    1530                         if ( expanded && ! section.hasClass( 'current-panel' ) ) {
    15311606                                // Collapse any sibling sections/panels
    15321607                                api.section.each( function ( otherSection ) {
    1533                                         if ( otherSection !== panel ) {
     1608                                        var searchTerm;
     1609
     1610                                        if ( otherSection !== section ) {
     1611
     1612                                                // Try to sync the current search term to the new section.
     1613                                                if ( 'themes' === otherSection.params.type ) {
     1614                                                        searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val();
     1615                                                        section.contentContainer.find( '.wp-filter-search' ).val( searchTerm );
     1616
     1617                                                        // Directly initialize an empty remote search to avoid a race condition.
     1618                                                        if ( '' === searchTerm && '' !== section.term && 'installed' !== section.params.action ) {
     1619                                                                section.term = '';
     1620                                                                section.initializeNewQuery( section.term, section.tags );
     1621                                                        } else {
     1622                                                                section.checkTerm( section );
     1623                                                        }
     1624                                                        section.filterSearch( section.contentContainer.find( '.wp-filter-search' ).get( 0 ) );
     1625                                                }
    15341626                                                otherSection.collapse( { duration: args.duration } );
    15351627                                        }
    15361628                                });
    1537                                 api.panel.each( function ( otherPanel ) {
    1538                                         otherPanel.collapse( { duration: 0 } );
    1539                                 });
    15401629
    1541                                 panel._animateChangeExpanded( function() {
    1542                                         changeBtn.attr( 'tabindex', '-1' );
    1543                                         customizeBtn.attr( 'tabindex', '0' );
     1630                                section.contentContainer.addClass( 'current-section' );
     1631                                container.scrollTop();
     1632                                section.headerContainer.find( '.customize-themes-section-title' ).addClass( 'selected' ).attr( 'aria-expanded', 'true' );
    15441633
    1545                                         customizeBtn.focus();
    1546                                         section.css( 'top', '' );
    1547                                         container.scrollTop( 0 );
     1634                                container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
     1635                                container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
    15481636
    1549                                         if ( args.completeCallback ) {
    1550                                                 args.completeCallback();
     1637                                if ( args.completeCallback ) {
     1638                                        args.completeCallback();
     1639                                }
     1640                                section.updateCount(); // Show this section's count.
     1641                        } else {
     1642                                section.contentContainer.removeClass( 'current-section' );
     1643
     1644                                // Always hide, even if they don't exist or are already hidden.
     1645                                section.headerContainer.find( '.customize-themes-section-title' ).removeClass( 'selected details-open' ).attr( 'aria-expanded', 'false' );
     1646                                section.headerContainer.find( '.filter-details' ).slideUp( 180 );
     1647
     1648                                container.off( 'scroll' );
     1649
     1650                                if ( args.completeCallback ) {
     1651                                        args.completeCallback();
     1652                                }
     1653                        }
     1654                },
     1655
     1656                /**
     1657                 * Return the section's content element without detachng from the parent.
     1658                 *
     1659                 * @since 4.9.0
     1660                 */
     1661                getContent: function() {
     1662                        return this.container.find( '.control-section-content' );
     1663                },
     1664
     1665                /**
     1666                 * Load theme data via Ajax and add themes to the section as controls.
     1667                 *
     1668                 * @since 4.9.0
     1669                 */
     1670                loadControls: function() {
     1671                        var section = this, params, page, request;
     1672
     1673                        if ( section.loading ) {
     1674                                return; // We're already loading a batch of themes.
     1675                        }
     1676
     1677                        // Parameters for every API query. Additional params are set in PHP.
     1678                        page = Math.ceil( section.loaded / 100 ) + 1;
     1679                        params = {
     1680                                'switch-themes-nonce': api.settings.nonce['switch-themes'],
     1681                                'wp_customize': 'on',
     1682                                'theme_action': section.params.action,
     1683                                'customized_theme': api.settings.theme.stylesheet,
     1684                                'page': page
     1685                        };
     1686
     1687                        // Add fields for wporg actions.
     1688                        if ( 'wporg' === section.params.action ) {
     1689                                        params.search = section.term;
     1690                                        params.tags = section.tags;
     1691                        }
     1692
     1693                        // Load themes.
     1694                        section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
     1695                        section.loading = true;
     1696                        section.container.find( '.no-themes' ).hide();
     1697                        request = wp.ajax.post( 'customize-load-themes', params );
     1698                        request.done(function( data ) {
     1699                                var themes = data.themes,
     1700                                    themeControl, newThemeControls;
     1701
     1702                                // Stop and try again if the term changed while loading.
     1703                                if ( section.nextTerm || section.nextTags ) {
     1704                                        if ( section.nextTerm ) {
     1705                                                section.term = section.nextTerm;
    15511706                                        }
    1552                                 } );
     1707                                        if ( section.nextTags ) {
     1708                                                section.tags = section.nextTags;
     1709                                        }
     1710                                        section.nextTerm = '';
     1711                                        section.nextTags = '';
     1712                                        section.loading = false;
     1713                                        section.loadControls();
     1714                                        return;
     1715                                }
    15531716
    1554                                 overlay.addClass( 'in-themes-panel' );
    1555                                 section.addClass( 'current-panel' );
    1556                                 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
    1557                                 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
     1717                                if ( 0 !== themes.length ) {
     1718                                        newThemeControls = [];
     1719
     1720                                        // Add controls for each theme.
     1721                                        _.each( themes, function( theme ) {
     1722                                                var customizeId = section.params.action + '_theme_' + theme.id;
     1723                                                themeControl = new api.controlConstructor.theme( customizeId, {
     1724                                                        params: {
     1725                                                                type: 'theme',
     1726                                                                content: '<li id="customize-control-theme-' + section.params.action + '_' + theme.id + '" class="customize-control customize-control-theme"></li>',
     1727                                                                section: section.params.id,
     1728                                                                active: true,
     1729                                                                theme: theme,
     1730                                                                priority: section.loaded + 1
     1731                                                        },
     1732                                                        previewer: api.previewer
     1733                                                } );
    15581734
    1559                         } else if ( ! expanded && section.hasClass( 'current-panel' ) ) {
    1560                                 panel._animateChangeExpanded( function() {
    1561                                         changeBtn.attr( 'tabindex', '0' );
    1562                                         customizeBtn.attr( 'tabindex', '-1' );
     1735                                                api.control.add( customizeId, themeControl );
     1736                                                newThemeControls.push( themeControl );
     1737                                                section.loaded = section.loaded + 1;
     1738                                        });
    15631739
    1564                                         changeBtn.focus();
    1565                                         section.css( 'top', '' );
     1740                                        if ( 1 === page ) {
    15661741
    1567                                         if ( args.completeCallback ) {
    1568                                                 args.completeCallback();
     1742                                                // Pre-load the first 3 theme screenshots.
     1743                                                _.each( section.controls().slice( 0, 3 ), function( control ) {
     1744                                                        var img, src = control.params.theme.screenshot[0];
     1745                                                        if ( src ) {
     1746                                                                img = new Image();
     1747                                                                img.src = src;
     1748                                                        }
     1749                                                });
     1750                                                if ( 'installed' !== section.params.action ) {
     1751                                                        wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
     1752                                                }
     1753                                        } else {
     1754                                                Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
    15691755                                        }
    1570                                 } );
     1756                                        _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
     1757
     1758                                        if ( 'installed' === section.params.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
     1759                                                section.fullyLoaded = true;
     1760                                        }
     1761                                } else {
     1762                                        if ( 0 === section.loaded ) {
     1763                                                section.container.find( '.no-themes' ).show();
     1764                                                wp.a11y.speak( section.container.find( '.no-themes' ).text() );
     1765                                        } else {
     1766                                                section.fullyLoaded = true;
     1767                                        }
     1768                                }
     1769                                if ( 'installed' === section.params.action ) {
     1770                                        section.updateCount(); // Count of visible theme controls.
     1771                                } else {
     1772                                        section.updateCount( data.info.results ); // Total number of results including pages not yet loaded.
     1773                                }
     1774                                section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
     1775
     1776                                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1777                                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1778                                section.loading = false;
     1779                        });
     1780                        request.fail(function( data ) {
     1781                                if ( 'undefined' === typeof data ) {
     1782                                        section.container.find( '.unexpected-error' ).show();
     1783                                        wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
     1784                                } else if ( 'undefined' !== typeof console && console.error ) {
     1785                                        console.error( data );
     1786                                }
     1787
     1788                                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
     1789                                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
     1790                                section.loading = false;
     1791                        });
     1792                },
     1793
     1794                /**
     1795                 * Determines whether more themes should be loaded, and loads them.
     1796                 *
     1797                 * @since 4.9.0
     1798                 */
     1799                loadMore: function() {
     1800                        var section = this, container, bottom, threshold;
     1801                        if ( ! section.fullyLoaded && ! section.loading ) {
     1802                                container = section.container.closest( '.customize-themes-full-container' );
    15711803
    1572                                 overlay.removeClass( 'in-themes-panel' );
    1573                                 section.removeClass( 'current-panel' );
    1574                                 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
     1804                                bottom = container.scrollTop() + container.height();
     1805                                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.
     1806
     1807                                if ( bottom > threshold ) {
     1808                                        section.loadControls();
     1809                                }
     1810                        }
     1811                },
     1812
     1813                /**
     1814                 * Event handler for search input that filters visible controls.
     1815                 *
     1816                 * @since 4.9.0
     1817                 *
     1818                 * @param Element el The search input element as a raw JS object.
     1819                 */
     1820                filterSearch: function( el ) {
     1821                        var count = 0,
     1822                                visible = false,
     1823                                section = this,
     1824                                noFilter = ( undefined !== api.section( 'wporg_themes' ) && 'wporg' !== section.params.action ) ? '.no-themes-local' : '.no-themes',
     1825                                term = el.value.toLowerCase().trim().replace( '-', ' ' ),
     1826                                controls = section.controls(),
     1827                                renderScreenshots;
     1828
     1829                        if ( section.loading ) {
     1830                                return;
     1831                        }
     1832
     1833                        _.each( controls, function( control ) {
     1834                                visible = control.filter( term );
     1835                                if ( visible ) {
     1836                                        count = count + 1;
     1837                                }
     1838                        });
     1839
     1840                        if ( 0 === count ) {
     1841                                section.container.find( noFilter ).show();
     1842                                wp.a11y.speak( section.container.find( noFilter ).text() );
     1843                        } else {
     1844                                section.container.find( noFilter ).hide();
     1845                        }
     1846
     1847                        renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
     1848
     1849                        renderScreenshots();
     1850
     1851                        // Update theme count.
     1852                        section.updateCount( count );
     1853                },
     1854
     1855                /**
     1856                 * Event handler for search input that determines if the terms have changed and loads new controls as needed.
     1857                 *
     1858                 * @since 4.9.0
     1859                 *
     1860                 * @param {wp.customize.ThemesSection} section The current theme section, passed through the debouncer.
     1861                 */
     1862                checkTerm: function( section ) {
     1863                        var newTerm;
     1864
     1865                        // Find term.
     1866                        if ( 'wporg' === section.params.action ) {
     1867                                newTerm = $( '#wp-filter-search-input' ).val();
     1868                        } else {
     1869                                return;
     1870                        }
     1871
     1872                        if ( section.term === newTerm ) {
     1873                                return;
     1874                        } else {
     1875                                section.initializeNewQuery( newTerm, section.tags );
     1876                        }
     1877
     1878                },
     1879
     1880                /**
     1881                 * Check for filters checked in the feature filter list and initialize a new query.
     1882                 *
     1883                 * @since 4.9.0
     1884                 */
     1885                filtersChecked: function() {
     1886                        var section = this,
     1887                            items = section.container.find( '.filter-group' ).find( ':checkbox' ),
     1888                            tags = [];
     1889
     1890                        _.each( items.filter( ':checked' ), function( item ) {
     1891                                tags.push( $( item ).prop( 'value' ) );
     1892                        });
     1893
     1894                        // When no filters are checked, restore initial state.
     1895                        if ( 0 === tags.length ) {
     1896                                tags = '';
     1897                                section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show();
     1898                                section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide();
     1899                        } else {
     1900                                section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
     1901                                section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide();
     1902                                section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show();
     1903                        }
     1904
     1905                        section.initializeNewQuery( section.term, tags );
     1906                },
     1907
     1908                /**
     1909                 * Reset the current query and load new results.
     1910                 *
     1911                 * @since 4.9.0
     1912                 */
     1913                initializeNewQuery: function( newTerm, newTags ) {
     1914                        var section = this;
     1915
     1916                        // Clear the controls in the section.
     1917                        _.each( section.controls(), function( control ) {
     1918                                control.container.remove();
     1919                                api.control.remove( control.id );
     1920                        });
     1921                        section.loaded = 0;
     1922                        section.fullyLoaded = false;
     1923                        section.screenshotQueue = null;
     1924
     1925                        // Run a new query, with loadControls handling paging, etc.
     1926                        if ( ! section.loading ) {
     1927                                section.term = newTerm;
     1928                                section.tags = newTags;
     1929                                section.loadControls();
     1930                        } else {
     1931                                section.nextTerm = newTerm; // This will reload from loadControls() with the newest term once the current batch is loaded.
     1932                                section.nextTags = newTags; // This will reload from loadControls() with the newest tags once the current batch is loaded.
     1933                        }
     1934                        if ( ! section.expanded() ) {
     1935                                section.expand(); // Expand the section if it isn't expanded.
    15751936                        }
    15761937                },
    15771938
     
    15801941                 *
    15811942                 * @since 4.2.0
    15821943                 */
    1583                 renderScreenshots: function( ) {
     1944                renderScreenshots: function() {
    15841945                        var section = this;
    15851946
    1586                         // Fill queue initially.
    1587                         if ( section.screenshotQueue === null ) {
    1588                                 section.screenshotQueue = section.controls();
     1947                        // Fill queue initially, or check for more if empty.
     1948                        if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) {
     1949
     1950                                // Add controls that haven't had their screenshots rendered.
     1951                                section.screenshotQueue = _.filter( section.controls(), function( control ) {
     1952                                        return ! control.screenshotRendered;
     1953                                });
    15891954                        }
    15901955
    1591                         // Are all screenshots rendered?
     1956                        // Are all screenshots rendered (for now)?
    15921957                        if ( ! section.screenshotQueue.length ) {
    15931958                                return;
    15941959                        }
     
    16231988                        } );
    16241989                },
    16251990
     1991                getVisibleCount: function() {
     1992                        return this.contentContainer.find( 'li.customize-control:visible' ).length;
     1993                },
     1994
     1995                /**
     1996                 * Update the number of themes in the section.
     1997                 *
     1998                 * @since 4.9.0
     1999                 */
     2000                updateCount: function( count ) {
     2001                        var section = this, countEl, displayed;
     2002
     2003                        if ( ! count && 0 !== count ) {
     2004                                count = section.getVisibleCount();
     2005                        }
     2006
     2007                        displayed = section.contentContainer.find( '.themes-displayed' );
     2008                        countEl = section.contentContainer.find( '.theme-count' );
     2009
     2010                        if ( 0 === count ) {
     2011                                countEl.text( '0' );
     2012                        } else {
     2013                                // Animate the count change for emphasis.
     2014                                displayed.fadeOut( 180, function() {
     2015                                        countEl.text( count );
     2016                                        displayed.fadeIn( 180 );
     2017                                } );
     2018                                wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
     2019                        }
     2020                },
     2021
    16262022                /**
    16272023                 * Advance the modal to the next theme.
    16282024                 *
     
    16432039                 * @since 4.2.0
    16442040                 */
    16452041                getNextTheme: function () {
    1646                         var control, next;
    1647                         control = api.control( 'theme_' + this.currentTheme );
     2042                        var section = this, control, next;
     2043                        control = api.control( section.params.action + '_theme_' + this.currentTheme );
    16482044                        next = control.container.next( 'li.customize-control-theme' );
    16492045                        if ( ! next.length ) {
    16502046                                return false;
    16512047                        }
    1652                         next = next[0].id.replace( 'customize-control-', '' );
     2048                        next = next[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    16532049                        control = api.control( next );
    16542050
    16552051                        return control.params.theme;
     
    16752071                 * @since 4.2.0
    16762072                 */
    16772073                getPreviousTheme: function () {
    1678                         var control, previous;
    1679                         control = api.control( 'theme_' + this.currentTheme );
     2074                        var section = this, control, previous;
     2075                        control = api.control( section.params.action + '_theme_' + this.currentTheme );
    16802076                        previous = control.container.prev( 'li.customize-control-theme' );
    16812077                        if ( ! previous.length ) {
    16822078                                return false;
    16832079                        }
    1684                         previous = previous[0].id.replace( 'customize-control-', '' );
     2080                        previous = previous[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
    16852081                        control = api.control( previous );
    16862082
    16872083                        return control.params.theme;
     
    17692165                 * @param {Object}   theme
    17702166                 */
    17712167                showDetails: function ( theme, callback ) {
    1772                         var section = this, link;
     2168                        var section = this;
    17732169                        callback = callback || function(){};
    17742170                        section.currentTheme = theme.id;
    17752171                        section.overlay.html( section.template( theme ) )
     
    17782174                        $( 'body' ).addClass( 'modal-open' );
    17792175                        section.containFocus( section.overlay );
    17802176                        section.updateLimits();
    1781 
    1782                         link = section.overlay.find( '.inactive-theme > a' );
    1783 
    1784                         link.on( 'click', function( event ) {
    1785                                 event.preventDefault();
    1786 
    1787                                 // Short-circuit if request is currently being made.
    1788                                 if ( link.hasClass( 'disabled' ) ) {
    1789                                         return;
    1790                                 }
    1791                                 link.addClass( 'disabled' );
    1792 
    1793                                 section.loadThemePreview( theme.id ).fail( function() {
    1794                                         link.removeClass( 'disabled' );
    1795                                 } );
    1796                         } );
     2177                        wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
    17972178                        callback();
    17982179                },
    17992180
     
    18052186                closeDetails: function () {
    18062187                        $( 'body' ).removeClass( 'modal-open' );
    18072188                        this.overlay.fadeOut( 'fast' );
    1808                         api.control( 'theme_' + this.currentTheme ).focus();
     2189                        api.control( this.params.action + '_theme_' + this.currentTheme ).container.find( '.theme' ).focus();
    18092190                },
    18102191
    18112192                /**
     
    18852266                        }
    18862267                        if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
    18872268                                container.append( panel.contentContainer );
    1888                                 panel.renderContent();
    18892269                        }
     2270                        panel.renderContent();
    18902271
    18912272                        panel.deferred.embedded.resolve();
    18922273                },
     
    20912472                }
    20922473        });
    20932474
     2475
     2476        /**
     2477         * wp.customize.ThemesPanel
     2478         *
     2479         * Custom section for themes that displays without the customize preview.
     2480         *
     2481         * @constructor
     2482         * @augments wp.customize.Panel
     2483         * @augments wp.customize.Container
     2484         */
     2485        api.ThemesPanel = api.Panel.extend({
     2486                installingThemes: [],
     2487
     2488                /**
     2489                 * @since 4.9.0
     2490                 */
     2491                attachEvents: function () {
     2492                        var panel = this;
     2493
     2494                        // Attach regular panel events.
     2495                        api.Panel.prototype.attachEvents.apply( this );
     2496
     2497                        // Collapse panel to customize the current theme.
     2498                        panel.contentContainer.on( 'click', '.customize-theme', function() {
     2499                                panel.collapse();
     2500                        });
     2501
     2502                        // Toggle between filtering and browsing themes on mobile.
     2503                        panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() {
     2504                                $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
     2505                        });
     2506
     2507                        // Install (and maybe preview) a theme.
     2508                        panel.contentContainer.on( 'click', '.theme-install', function( event ) {
     2509                                panel.installTheme( event );
     2510                        });
     2511
     2512                        // Update a theme. Theme cards have the class, the details modal has the id.
     2513                        panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
     2514
     2515                                // #update-theme is a link.
     2516                                event.preventDefault();
     2517                                event.stopPropagation();
     2518
     2519                                panel.updateTheme( event );
     2520                        });
     2521
     2522                        // Delete a theme.
     2523                        panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
     2524                                panel.deleteTheme( event );
     2525                        });
     2526
     2527                        _.bindAll( this, 'installTheme', 'updateTheme' );
     2528                },
     2529
     2530                /**
     2531                 * Update UI to reflect expanded state
     2532                 *
     2533                 * @since 4.9.0
     2534                 *
     2535                 * @param {Boolean}  expanded
     2536                 * @param {Object}   args
     2537                 * @param {Boolean}  args.unchanged
     2538                 * @param {Function} args.completeCallback
     2539                 */
     2540                onChangeExpanded: function ( expanded, args ) {
     2541
     2542                        // Expand/collapse the panel normally.
     2543                        api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
     2544
     2545                        // Immediately call the complete callback if there were no changes
     2546                        if ( args.unchanged ) {
     2547                                if ( args.completeCallback ) {
     2548                                        args.completeCallback();
     2549                                }
     2550                                return;
     2551                        }
     2552
     2553                        // Note: there is a second argument 'args' passed
     2554                        var panel = this,
     2555                                overlay = panel.headContainer.closest( '.wp-full-overlay' );
     2556
     2557                        if ( expanded ) {
     2558                                overlay
     2559                                        .addClass( 'in-themes-panel' )
     2560                                        .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
     2561
     2562                                // Automatically open the installed themes section (except on small screens).
     2563                                if ( 600 < window.innerWidth ) {
     2564                                        api.section( 'installed_themes' ).expand();
     2565                                }
     2566                        } else {
     2567                                overlay
     2568                                        .removeClass( 'in-themes-panel' )
     2569                                        .find( '.customize-themes-full-container' ).removeClass( 'animate' );
     2570                        }
     2571                },
     2572
     2573                /**
     2574                 * Install a theme via wp.updates.
     2575                 *
     2576                 * @since 4.9.0
     2577                 */
     2578                installTheme: function( event ) {
     2579                        var panel = this, preview = false, slug = $( event.target ).data( 'slug' );
     2580
     2581                        if ( -1 !== $.inArray( this.installingThemes, slug ) ) {
     2582                                return; // Theme is already being installed.
     2583                        }
     2584
     2585                        wp.updates.maybeRequestFilesystemCredentials( event );
     2586
     2587                        $( document ).one( 'wp-theme-install-success', function( event, response ) {
     2588                                var theme = false, customizeId, themeControl;
     2589                                if ( preview ) {
     2590
     2591                                        panel.loadThemePreview( slug ).fail( function() {
     2592                                                $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
     2593                                        } );
     2594
     2595                                } else {
     2596                                        api.control.each( function( control ) {
     2597                                                if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2598                                                        theme = control.params.theme; // Used below to add theme control.
     2599                                                        control.rerenderAsInstalled( true );
     2600                                                }
     2601                                        });
     2602
     2603                                        // Don't add the same theme more than once.
     2604                                        if ( ! theme || 'undefined' !== typeof api.control( 'installed_theme_' + theme.id ) ) {
     2605                                                return;
     2606                                        }
     2607
     2608                                        // Add theme control to installed section.
     2609                                        theme.type = 'installed';
     2610                                        customizeId = 'installed_theme_' + theme.id;
     2611                                        themeControl = new api.controlConstructor.theme( customizeId, {
     2612                                                params: {
     2613                                                        type: 'theme',
     2614                                                        content: $( '<li class="customize-control customize-control-theme"></li>' ).attr( 'id', 'customize-control-theme-installed_' + theme.id ).prop( 'outerHTML' ),
     2615                                                        section: 'installed_themes',
     2616                                                        active: true,
     2617                                                        theme: theme,
     2618                                                        priority: 0 // Add all newly-installed themes to the top.
     2619                                                },
     2620                                                previewer: api.previewer
     2621                                        } );
     2622
     2623                                        api.control.add( customizeId, themeControl );
     2624                                        api.control( customizeId ).container.trigger( 'render-screenshot' );
     2625
     2626                                        // Close the details modal if it's open to the installed theme.
     2627                                        api.section.each( function( section ) {
     2628                                                if ( 'themes' === section.params.type ) {
     2629                                                        if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
     2630                                                                section.closeDetails();
     2631                                                        }
     2632                                                }
     2633                                        });
     2634                                }
     2635                        } );
     2636
     2637                        this.installingThemes.push( $( event.target ).data( 'slug' ) ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
     2638                        wp.updates.installTheme( {
     2639                                slug: slug
     2640                        } );
     2641
     2642                        // Also preview the theme as the event is triggered on Install & Preview.
     2643                        if ( $( event.target ).hasClass( 'preview' ) ) {
     2644                                preview = true;
     2645                                $( '.wp-full-overlay' ).addClass( 'customize-loading' );
     2646                                wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text-installing-theme' ).text() );
     2647                        }
     2648                },
     2649
     2650                /**
     2651                 * Load theme preview.
     2652                 *
     2653                 * @since 4.9.0
     2654                 *
     2655                 * @param {string} themeId Theme ID.
     2656                 * @returns {jQuery.promise} Promise.
     2657                 */
     2658                loadThemePreview: function( themeId ) {
     2659                        var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
     2660
     2661                        urlParser = document.createElement( 'a' );
     2662                        urlParser.href = location.href;
     2663                        urlParser.search = $.param( _.extend(
     2664                                api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
     2665                                {
     2666                                        theme: themeId,
     2667                                        changeset_uuid: api.settings.changeset.uuid
     2668                                }
     2669                        ) );
     2670
     2671                        // Update loading message. Everything else is handled by reloading the page.
     2672                        $( '#customize-themes-loading-container span' ).hide();
     2673                        $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' );
     2674                        wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text' ).text() );
     2675                        overlay = $( '.wp-full-overlay' );
     2676                        overlay.addClass( 'customize-loading' );
     2677
     2678                        onceProcessingComplete = function() {
     2679                                var request;
     2680                                if ( api.state( 'processing' ).get() > 0 ) {
     2681                                        return;
     2682                                }
     2683
     2684                                api.state( 'processing' ).unbind( onceProcessingComplete );
     2685
     2686                                request = api.requestChangesetUpdate();
     2687                                request.done( function() {
     2688                                        $( window ).off( 'beforeunload.customize-confirm' );
     2689                                        window.location.href = urlParser.href;
     2690                                } );
     2691                                request.fail( function() {
     2692                                        overlay.removeClass( 'customize-loading' );
     2693                                } );
     2694                        };
     2695
     2696                        if ( 0 === api.state( 'processing' ).get() ) {
     2697                                onceProcessingComplete();
     2698                        } else {
     2699                                api.state( 'processing' ).bind( onceProcessingComplete );
     2700                        }
     2701
     2702                        return deferred.promise();
     2703                },
     2704
     2705                /**
     2706                 * Update a theme via wp.updates.
     2707                 *
     2708                 * @since 4.9.0
     2709                 */
     2710                updateTheme: function( event ) {
     2711                        wp.updates.maybeRequestFilesystemCredentials( event );
     2712
     2713                        $( document ).one( 'wp-theme-update-success', function( event, response ) {
     2714
     2715                                // Rerender the control to reflect the update.
     2716                                api.control.each( function( control ) {
     2717                                        if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
     2718                                                control.params.theme.hasUpdate = false;
     2719                                                control.rerenderAsInstalled( true );
     2720                                        }
     2721                                });
     2722                        } );
     2723
     2724                        wp.updates.updateTheme( {
     2725                                slug: $( event.target ).closest( '.notice' ).data( 'slug' )
     2726                        } );
     2727                },
     2728
     2729                /**
     2730                 * Delete a theme via wp.updates.
     2731                 *
     2732                 * @since 4.9.0
     2733                 */
     2734                deleteTheme: function( event ) {
     2735                        var theme, section;
     2736                        theme = $( event.target ).data( 'slug' );
     2737                        section = api.section( 'installed_themes' );
     2738
     2739                        event.preventDefault();
     2740
     2741                        // Confirmation dialog for deleting a theme.
     2742                        if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
     2743                                return;
     2744                        }
     2745
     2746                        wp.updates.maybeRequestFilesystemCredentials( event );
     2747
     2748                        $( document ).one( 'wp-theme-delete-success', function() {
     2749                                var control = api.control( 'installed_theme_' + theme );
     2750
     2751                                // Remove theme control.
     2752                                control.container.remove();
     2753                                api.control.remove( control.id );
     2754
     2755                                // Update installed count.
     2756                                section.loaded = section.loaded - 1;
     2757                                section.updateCount();
     2758
     2759                                // Rerender any other theme controls as uninstalled.
     2760                                api.control.each( function( control ) {
     2761                                        if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
     2762                                                control.rerenderAsInstalled( false );
     2763                                        }
     2764                                });
     2765                        } );
     2766
     2767                        wp.updates.deleteTheme( {
     2768                                slug: theme
     2769                        } );
     2770
     2771                        // Close modal and focus the section.
     2772                        section.closeDetails();
     2773                        section.focus();
     2774                }
     2775
     2776        });
     2777
    20942778        /**
    20952779         * A Customizer Control.
    20962780         *
     
    24403124                 * @param {Boolean}  active
    24413125                 * @param {Object}   args
    24423126                 * @param {Number}   args.duration
    2443                  * @param {Callback} args.completeCallback
     3127                 * @param {Function} args.completeCallback
    24443128                 */
    24453129                onChangeActive: function ( active, args ) {
    24463130                        if ( args.unchanged ) {
     
    36124296        api.ThemeControl = api.Control.extend({
    36134297
    36144298                touchDrag: false,
    3615                 isRendered: false,
    3616 
    3617                 /**
    3618                  * Defer rendering the theme control until the section is displayed.
    3619                  *
    3620                  * @since 4.2.0
    3621                  */
    3622                 renderContent: function () {
    3623                         var control = this,
    3624                                 renderContentArgs = arguments;
    3625 
    3626                         api.section( control.section(), function( section ) {
    3627                                 if ( section.expanded() ) {
    3628                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
    3629                                         control.isRendered = true;
    3630                                 } else {
    3631                                         section.expanded.bind( function( expanded ) {
    3632                                                 if ( expanded && ! control.isRendered ) {
    3633                                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
    3634                                                         control.isRendered = true;
    3635                                                 }
    3636                                         } );
    3637                                 }
    3638                         } );
    3639                 },
     4299                screenshotRendered: false,
    36404300
    36414301                /**
    36424302                 * @since 4.2.0
     
    36604320                                }
    36614321
    36624322                                // Prevent the modal from showing when the user clicks the action button.
    3663                                 if ( $( event.target ).is( '.theme-actions .button' ) ) {
    3664                                         return;
    3665                                 }
    3666 
    3667                                 api.section( control.section() ).loadThemePreview( control.params.theme.id );
    3668                         });
    3669 
    3670                         control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
    3671                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
     4323                                if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
    36724324                                        return;
    36734325                                }
    36744326
    36754327                                event.preventDefault(); // Keep this AFTER the key filter above
    3676 
    36774328                                api.section( control.section() ).showDetails( control.params.theme );
    36784329                        });
    36794330
     
    36844335                                if ( source ) {
    36854336                                        $screenshot.attr( 'src', source );
    36864337                                }
     4338                                control.screenshotRendered = true;
    36874339                        });
    36884340                },
    36894341
    36904342                /**
    3691                  * Show or hide the theme based on the presence of the term in the title, description, and author.
     4343                 * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
    36924344                 *
    36934345                 * @since 4.2.0
    36944346                 */
     
    37014353                        haystack = haystack.toLowerCase().replace( '-', ' ' );
    37024354                        if ( -1 !== haystack.search( term ) ) {
    37034355                                control.activate();
     4356                                return true;
    37044357                        } else {
    37054358                                control.deactivate();
     4359                                return false;
     4360                        }
     4361                },
     4362
     4363                /**
     4364                 * Rerender the theme from its JS template with the installed type.
     4365                 *
     4366                 * @since 4.9.0
     4367                 */
     4368                rerenderAsInstalled: function( installed ) {
     4369                        var control = this, section;
     4370                        if ( installed ) {
     4371                                control.params.theme.type = 'installed';
     4372                        } else {
     4373                                section = api.section( control.params.section );
     4374                                control.params.theme.type = section.params.action;
    37064375                        }
     4376                        control.renderContent(); // Replaces existing content.
     4377                        control.container.trigger( 'render-screenshot' );
    37074378                }
    37084379        });
    37094380
     
    46625333                theme:               api.ThemeControl,
    46635334                code_editor:         api.CodeEditorControl
    46645335        };
    4665         api.panelConstructor = {};
     5336        api.panelConstructor = {
     5337                themes: api.ThemesPanel
     5338        };
    46665339        api.sectionConstructor = {
    46675340                themes: api.ThemesSection
    46685341        };
     
    47805453
    47815454                // Sort the sections within each panel
    47825455                api.panel.each( function ( panel ) {
     5456                        if ( 'themes' === panel.id ) {
     5457                                return; // Don't reflow theme sections, as doing so moves them after the themes container.
     5458                        }
     5459
    47835460                        var sections = panel.sections(),
    47845461                                sectionHeadContainers = _.pluck( sections, 'headContainer' );
    47855462                        rootNodes.push( panel );
     
    56256302                        // Collapse the most granular expanded object.
    56266303                        collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
    56276304                        if ( collapsedObject ) {
     6305                                if ( 'themes' === collapsedObject.params.type ) {
     6306
     6307                                        // Themes panel or section.
     6308                                        if ( $( 'body' ).hasClass( 'modal-open' ) ) {
     6309                                                collapsedObject.closeDetails();
     6310                                        } else {
     6311
     6312                                                // If we're collapsing a section, collapse the panel also.
     6313                                                wp.customize.panel( 'themes' ).collapse();
     6314                                        }
     6315                                        return;
     6316                                }
    56286317                                collapsedObject.collapse();
    56296318                                event.preventDefault();
    56306319                        }
  • src/wp-admin/js/updates.js

    diff --git a/src/wp-admin/js/updates.js b/src/wp-admin/js/updates.js
    index 4f1bfbbdc2..725cdbc170 100644
    a b  
    183183                if ( $notice.length ) {
    184184                        $notice.replaceWith( $adminNotice );
    185185                } else {
    186                         $( '.wrap' ).find( '> h1' ).after( $adminNotice );
     186                        if ( 'customize' === pagenow ) {
     187                                $( '.customize-themes-notifications' ).append( $adminNotice );
     188                        } else {
     189                                $( '.wrap' ).find( '> h1' ).after( $adminNotice );
     190                        }
    187191                }
    188192
    189193                $document.trigger( 'wp-updates-notice-added' );
     
    930934                if ( 'themes-network' === pagenow ) {
    931935                        $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
    932936
     937                } else if ( 'customize' === pagenow ) {
     938
     939                        // Update the theme details UI.
     940                        $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
     941
     942                        $notice.find( 'h3' ).remove();
     943
     944                        // Add the top-level UI, and update both.
     945                        $notice = $notice.add( $( '#customize-control-theme-installed_' + args.slug ).find( '.update-message' ) );
     946                        $notice = $notice.addClass( 'updating-message' ).find( 'p' );
     947
    933948                } else {
    934949                        $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
    935950
     
    972987                        },
    973988                        $notice, newText;
    974989
     990                if ( 'customize' === pagenow ) {
     991                        $theme = wp.customize.control( 'installed_theme_' + response.slug ).container;
     992                }
     993
    975994                if ( 'themes-network' === pagenow ) {
    976995                        $notice = $theme.find( '.update-message' );
    977996
     
    10261045                        return;
    10271046                }
    10281047
     1048                if ( 'customize' === pagenow ) {
     1049                        $theme = wp.customize.control( 'installed_theme_' + response.slug ).container;
     1050                }
     1051
    10291052                if ( 'themes-network' === pagenow ) {
    10301053                        $notice = $theme.find( '.update-message ' );
    10311054                } else {
     
    11621185                        return;
    11631186                }
    11641187
    1165                 if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
    1166                         $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
    1167                         $card   = $( '.install-theme-info' ).prepend( $message );
     1188                if ( 'customize' === pagenow ) {
     1189                        if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) {
     1190                                $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
     1191                                $card   = $( '.theme-overlay .theme-info' ).prepend( $message );
     1192                        } else {
     1193                                $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
     1194                                $card   = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message );
     1195                        }
     1196                        $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
    11681197                } else {
    1169                         $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
    1170                         $button = $card.find( '.theme-install' );
     1198                        if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
     1199                                $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
     1200                                $card   = $( '.install-theme-info' ).prepend( $message );
     1201                        } else {
     1202                                $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
     1203                                $button = $card.find( '.theme-install' );
     1204                        }
    11711205                }
    11721206
    11731207                $button
  • src/wp-includes/class-wp-customize-manager.php

    diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php
    index 33f423aacb..5f146bb239 100644
    a b public function __construct( $args = array() ) { 
    320320
    321321                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
    322322
     323                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
    323324                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
    324325                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
    325326                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
    public function __construct( $args = array() ) { 
    375376
    376377                add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
    377378                add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
     379                add_action( 'wp_ajax_customize-load-themes',    array( $this, 'load_themes_ajax' ) );
    378380                add_action( 'wp_ajax_dismiss_customize_changeset_autosave', array( $this, 'handle_dismiss_changeset_autosave_request' ) );
    379381
    380382                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
    public function __construct( $args = array() ) { 
    392394
    393395                // Export the settings to JS via the _wpCustomizeSettings variable.
    394396                add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
     397
     398                // Add theme update notices.
     399                if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
     400                        require_once ABSPATH . '/wp-admin/includes/update.php';
     401                        add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
     402                }
    395403        }
    396404
    397405        /**
    public function enqueue_control_scripts() { 
    36223630                foreach ( $this->controls as $control ) {
    36233631                        $control->enqueue();
    36243632                }
     3633
     3634                if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
     3635                        wp_enqueue_script( 'updates' );
     3636                }
    36253637        }
    36263638
    36273639        /**
    public function get_nonces() { 
    38263838                $nonces = array(
    38273839                        'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
    38283840                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
     3841                        'switch-themes' => wp_create_nonce( 'switch-themes' ),
    38293842                        'dismiss_autosave' => wp_create_nonce( 'dismiss_customize_changeset_autosave' ),
    38303843                );
    38313844
    public function customize_pane_settings() { 
    39223935                        'autofocus' => $this->get_autofocus(),
    39233936                        'documentTitleTmpl' => $this->get_document_title_template(),
    39243937                        'previewableDevices' => $this->get_previewable_devices(),
     3938                        'l10n' => array(
     3939                                'confirmDeleteTheme' => __( 'Are you sure you want to delete this theme?' ),
     3940                                /* translators: %d is the number of theme search results, which cannot currently consider singular vs. plural forms */
     3941                                'themeSearchResults' => __( '%d themes found' ),
     3942                                /* translators: %d is the number of themes being displayed, which cannot currently consider singular vs. plural forms */
     3943                                'announceThemeCount' => __( 'Displaying %d themes' ),
     3944                                /* translators: %s is the theme name */
     3945                                'announceThemeDetails' => __( 'Showing details for theme: %s' ),
     3946                        ),
    39253947                );
    39263948
    39273949                // Prepare Customize Section objects to pass to JavaScript.
    public function register_controls() { 
    40244046
    40254047                /* Panel, Section, and Control Types */
    40264048                $this->register_panel_type( 'WP_Customize_Panel' );
     4049                $this->register_panel_type( 'WP_Customize_Themes_Panel' );
    40274050                $this->register_section_type( 'WP_Customize_Section' );
    40284051                $this->register_section_type( 'WP_Customize_Sidebar_Section' );
     4052                $this->register_section_type( 'WP_Customize_Themes_Section' );
    40294053                $this->register_control_type( 'WP_Customize_Color_Control' );
    40304054                $this->register_control_type( 'WP_Customize_Media_Control' );
    40314055                $this->register_control_type( 'WP_Customize_Upload_Control' );
    public function register_controls() { 
    40374061                $this->register_control_type( 'WP_Customize_Theme_Control' );
    40384062                $this->register_control_type( 'WP_Customize_Code_Editor_Control' );
    40394063
    4040                 /* Themes */
     4064                /* Themes (controls are loaded via ajax) */
    40414065
    4042                 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
    4043                         'title'      => $this->theme()->display( 'Name' ),
    4044                         'capability' => 'switch_themes',
    4045                         'priority'   => 0,
     4066                $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array(
     4067                        'title'       => $this->theme()->display( 'Name' ),
     4068                        '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.' ),
     4069                        'capability'  => 'switch_themes',
     4070                        'priority'    => 0,
    40464071                ) ) );
    40474072
    4048                 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
    4049                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
    4050                         'capability' => 'switch_themes',
     4073                $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array(
     4074                        'title'       => __( 'Installed themes' ),
     4075                        'action'      => 'installed',
     4076                        'capability'  => 'switch_themes',
     4077                        'panel'       => 'themes',
     4078                        'priority'    => 0,
    40514079                ) ) );
    40524080
    4053                 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
    4054 
    4055                 // Theme Controls.
    4056 
    4057                 // Add a control for the active/original theme.
    4058                 if ( ! $this->is_theme_active() ) {
    4059                         $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
    4060                         $active_theme = current( $themes );
    4061                         $active_theme['isActiveTheme'] = true;
    4062                         $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
    4063                                 'theme'    => $active_theme,
    4064                                 'section'  => 'themes',
    4065                                 'settings' => 'active_theme',
     4081                if ( ! is_multisite() ) {
     4082                        $this->add_section( new WP_Customize_Themes_Section( $this, 'wporg_themes', array(
     4083                                'title'       => __( 'WordPress.org themes' ),
     4084                                'action'      => 'wporg',
     4085                                'capability'  => 'install_themes',
     4086                                'panel'       => 'themes',
     4087                                'priority'    => 5,
    40664088                        ) ) );
    40674089                }
    40684090
    4069                 $themes = wp_prepare_themes_for_js();
    4070                 foreach ( $themes as $theme ) {
    4071                         if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
    4072                                 continue;
    4073                         }
    4074 
    4075                         $theme_id = 'theme_' . $theme['id'];
    4076                         $theme['isActiveTheme'] = false;
    4077                         $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
    4078                                 'theme'    => $theme,
    4079                                 'section'  => 'themes',
    4080                                 'settings' => 'active_theme',
    4081                         ) ) );
    4082                 }
     4091                // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
     4092                $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
     4093                        'capability' => 'switch_themes',
     4094                ) ) );
    40834095
    40844096                /* Site Identity */
    40854097
    public function register_dynamic_settings() { 
    45864598        }
    45874599
    45884600        /**
     4601         * Load themes into the theme browsing/installation UI.
     4602         *
     4603         * @since 4.9.0
     4604         */
     4605        public function load_themes_ajax() {
     4606                check_ajax_referer( 'switch-themes', 'switch-themes-nonce' );
     4607
     4608                if ( ! current_user_can( 'switch_themes' ) ) {
     4609                        wp_die( -1 );
     4610                }
     4611
     4612                if ( empty( $_POST['theme_action'] ) ) {
     4613                        wp_send_json_error( 'missing_theme_action' );
     4614                }
     4615
     4616                require_once ABSPATH . 'wp-admin/includes/theme.php';
     4617                if ( 'installed' === $_POST['theme_action'] ) {
     4618                        $themes = array( 'themes' => wp_prepare_themes_for_js() );
     4619                        foreach ( $themes['themes'] as &$theme ) {
     4620                                $theme['type'] = 'installed';
     4621                                // Set active based on customized theme.
     4622                                if ( $_POST['customized_theme'] === $theme['id'] ) {
     4623                                        $theme['active'] = true;
     4624                                } else {
     4625                                        $theme['active'] = false;
     4626                                }
     4627                        }
     4628                } elseif ( 'wporg' === $_POST['theme_action'] ) {
     4629                        if ( ! current_user_can( 'install_themes' ) ) {
     4630                                wp_die( -1 );
     4631                        }
     4632
     4633                        // Arguments for all queries.
     4634                        $args = array(
     4635                                'per_page' => 100,
     4636                                'page' => absint( $_POST['page'] ),
     4637                                'fields' => array(
     4638                                        'screenshot_url' => true,
     4639                                        'description' => true,
     4640                                        'rating' => true,
     4641                                        'downloaded' => true,
     4642                                        'downloadlink' => true,
     4643                                        'last_updated' => true,
     4644                                        'homepage' => true,
     4645                                        'num_ratings' => true,
     4646                                        'tags' => true,
     4647                                        'parent' => true,
     4648                                        // 'extended_author' => true, @todo: WordPress.org throws a 500 server error when this is here.
     4649                                ),
     4650                        );
     4651
     4652                        // Define query filters based on user input.
     4653                        if ( ! array_key_exists( 'search', $_POST ) ) {
     4654                                $args['search'] = '';
     4655                        } else {
     4656                                $args['search'] = wp_unslash( $_POST['search'] );
     4657                        }
     4658
     4659                        if ( ! array_key_exists( 'tags', $_POST ) ) {
     4660                                $args['tag'] = '';
     4661                        } else {
     4662                                $args['tag'] = wp_unslash( $_POST['tags'] );
     4663                        }
     4664
     4665                        if ( '' === $args['search'] && '' === $args['tag'] ) {
     4666                                $args['browse'] = 'new'; // Sort by latest themes by default.
     4667                        }
     4668
     4669                        // Load themes from the .org API.
     4670                        $themes = themes_api( 'query_themes', $args );
     4671                        if ( is_wp_error( $themes ) ) {
     4672                                wp_send_json_error();
     4673                        }
     4674
     4675                        // This list matches the allowed tags in wp-admin/includes/theme-install.php.
     4676                        $themes_allowedtags = array('a' => array('href' => array(), 'title' => array(), 'target' => array()),
     4677                                'abbr' => array('title' => array()), 'acronym' => array('title' => array()),
     4678                                'code' => array(), 'pre' => array(), 'em' => array(), 'strong' => array(),
     4679                                'div' => array(), 'p' => array(), 'ul' => array(), 'ol' => array(), 'li' => array(),
     4680                                'h1' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'h5' => array(), 'h6' => array(),
     4681                                'img' => array('src' => array(), 'class' => array(), 'alt' => array())
     4682                        );
     4683
     4684                        // Prepare a list of installed themes to check against before the loop.
     4685                        $installed_themes = array();
     4686                        $wp_themes = wp_get_themes();
     4687                        foreach ( $wp_themes as $theme ) {
     4688                                $installed_themes[] = $theme->get_stylesheet();
     4689                        }
     4690                        $update_php = network_admin_url( 'update.php?action=install-theme' );
     4691
     4692                        // Set up properties for themes available on WordPress.org.
     4693                        foreach ( $themes->themes as &$theme ) {
     4694                                $theme->install_url = add_query_arg( array(
     4695                                        'theme'    => $theme->slug,
     4696                                        '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
     4697                                ), $update_php );
     4698
     4699                                $theme->name        = wp_kses( $theme->name, $themes_allowedtags );
     4700                                $theme->author      = wp_kses( $theme->author, $themes_allowedtags );
     4701                                $theme->version     = wp_kses( $theme->version, $themes_allowedtags );
     4702                                $theme->description = wp_kses( $theme->description, $themes_allowedtags );
     4703                                $theme->tags        = implode( ', ', $theme->tags );
     4704                                $theme->stars       = wp_star_rating( array( 'rating' => $theme->rating, 'type' => 'percent', 'number' => $theme->num_ratings, 'echo' => false ) );
     4705                                $theme->num_ratings = number_format_i18n( $theme->num_ratings );
     4706                                $theme->preview_url = set_url_scheme( $theme->preview_url );
     4707
     4708                                // Handle themes that are already installed as installed themes.
     4709                                if ( in_array( $theme->slug, $installed_themes, true ) ) {
     4710                                        $theme->type = 'installed';
     4711                                } else {
     4712                                        $theme->type = $_POST['theme_action'];
     4713                                }
     4714
     4715                                // Set active based on customized theme.
     4716                                if ( $_POST['customized_theme'] === $theme->slug ) {
     4717                                        $theme->active = true;
     4718                                } else {
     4719                                        $theme->active = false;
     4720                                }
     4721
     4722                                // Map available theme properties to installed theme properties.
     4723                                $theme->id           = $theme->slug;
     4724                                $theme->screenshot   = array( $theme->screenshot_url );
     4725                                $theme->authorAndUri = $theme->author;
     4726                                $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.
     4727                                unset( $theme->slug );
     4728                                unset( $theme->screenshot_url );
     4729                                unset( $theme->author );
     4730                        } // End foreach().
     4731                } // End if().
     4732                wp_send_json_success( $themes );
     4733        }
     4734
     4735
     4736        /**
    45894737         * Callback for validating the header_textcolor value.
    45904738         *
    45914739         * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
  • src/wp-includes/customize/class-wp-customize-theme-control.php

    diff --git a/src/wp-includes/customize/class-wp-customize-theme-control.php b/src/wp-includes/customize/class-wp-customize-theme-control.php
    index ffc9c878ae..e7ac63274f 100644
    a b public function render_content() {} 
    5757         * @since 4.2.0
    5858         */
    5959        public function content_template() {
    60                 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
    61                 $active_url  = esc_url( remove_query_arg( 'customize_theme', $current_url ) );
    62                 $preview_url = esc_url( add_query_arg( 'customize_theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
    63                 $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url );
     60                /* translators: %s: theme name */
     61                $details_label = sprintf( __( 'Details for theme: %s' ), '{{ data.theme.name }}' );
     62                /* translators: %s: theme name */
     63                $customize_label = sprintf( __( 'Customize theme: %s' ), '{{ data.theme.name }}' );
     64                /* translators: %s: theme name */
     65                $preview_label = sprintf( __( 'Live preview theme: %s' ), '{{ data.theme.name }}' );
     66                /* translators: %s: theme name */
     67                $install_label = sprintf( __( 'Install and preview theme: %s' ), '{{ data.theme.name }}' );
    6468                ?>
    65                 <# if ( data.theme.isActiveTheme ) { #>
    66                         <div class="theme active" tabindex="0" data-preview-url="<?php echo esc_attr( $active_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
     69                <# if ( data.theme.active ) { #>
     70                        <div class="theme active" tabindex="0" aria-describedby="{{ data.section }}-{{ data.theme.id }}-action {{ data.theme.id }}-name">
    6771                <# } else { #>
    68                         <div class="theme" tabindex="0" data-preview-url="<?php echo esc_attr( $preview_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
     72                        <div class="theme" tabindex="0" aria-describedby="{{ data.section }}-{{ data.theme.id }}-action {{ data.theme.id }}-name">
    6973                <# } #>
    7074
    71                         <# if ( data.theme.screenshot[0] ) { #>
     75                        <# if ( data.theme.screenshot && data.theme.screenshot[0] ) { #>
    7276                                <div class="theme-screenshot">
    7377                                        <img data-src="{{ data.theme.screenshot[0] }}" alt="" />
    7478                                </div>
    public function content_template() { 
    7680                                <div class="theme-screenshot blank"></div>
    7781                        <# } #>
    7882
    79                         <# if ( data.theme.isActiveTheme ) { #>
    80                                 <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Customize' ); ?></span>
    81                         <# } else { #>
    82                                 <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Live Preview' ); ?></span>
    83                         <# } #>
     83                        <span class="more-details theme-details" id="{{ data.section }}-{{ data.theme.id }}-action" aria-label="<?php echo esc_attr( $details_label ); ?>"><?php _e( 'Theme Details' ); ?></span>
    8484
    8585                        <div class="theme-author"><?php
    8686                                /* translators: Theme author name */
    8787                                printf( _x( 'By %s', 'theme author' ), '{{ data.theme.author }}' );
    8888                        ?></div>
    8989
    90                         <# if ( data.theme.isActiveTheme ) { #>
    91                                 <h3 class="theme-name" id="{{ data.theme.id }}-name">
     90                        <# if ( 'installed' === data.theme.type && data.theme.hasUpdate ) { #>
     91                                <div class="update-message notice inline notice-warning notice-alt" data-slug="{{ data.theme.id }}"><p><?php
     92                                        /* translators: %s is the linked update now button */
     93                                        printf( __( 'New version available. %s' ), '<button class="button-link update-theme" type="button">' . __( 'Update now' ) . '</button>' ); ?></p></div>
     94                        <# } #>
     95
     96                        <# if ( data.theme.active ) { #>
     97                                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">
    9298                                        <?php
    9399                                        /* translators: %s: theme name */
    94                                         printf( __( '<span>Active:</span> %s' ), '{{{ data.theme.name }}}' );
     100                                        printf( __( '<span>Current:</span> %s' ), '{{ data.theme.name }}' );
    95101                                        ?>
    96102                                </h3>
     103                                <div class="theme-actions">
     104                                        <button type="button" class="button button-primary customize-theme" aria-label="<?php echo esc_attr( $customize_label ); ?>"><?php _e( 'Customize' ); ?></button>
     105                                </div>
     106                                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
     107                        <# } else if ( 'installed' === data.theme.type ) { #>
     108                                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
     109                                <div class="theme-actions">
     110                                        <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>
     111                                </div>
     112                                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
    97113                        <# } else { #>
    98                                 <h3 class="theme-name" id="{{ data.theme.id }}-name">{{{ data.theme.name }}}</h3>
     114                                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
    99115                                <div class="theme-actions">
    100                                         <button type="button" class="button theme-details"><?php _e( 'Theme Details' ); ?></button>
     116                                        <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>
    101117                                </div>
    102118                        <# } #>
    103119                </div>
  • new file src/wp-includes/customize/class-wp-customize-themes-panel.php

    diff --git a/src/wp-includes/customize/class-wp-customize-themes-panel.php b/src/wp-includes/customize/class-wp-customize-themes-panel.php
    new file mode 100644
    index 0000000000..7e197307c3
    - +  
     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 if ( current_user_can( 'switch_themes' ) ) : ?>
     49                                        <button type="button" class="button change-theme" aria-label="<?php _e( 'Change theme' ); ?>"><?php _ex( 'Change', 'theme' ); ?></button>
     50                                <?php endif; ?>
     51                        </h3>
     52                        <ul class="accordion-sub-container control-panel-content"></ul>
     53                </li>
     54                <?php
     55        }
     56
     57        /**
     58         * An Underscore (JS) template for this panel's content (but not its container).
     59         *
     60         * Class variables for this panel class are available in the `data` JS object;
     61         * export custom variables by overriding WP_Customize_Panel::json().
     62         *
     63         * @since 4.9.0
     64         *
     65         * @see WP_Customize_Panel::print_template()
     66         */
     67        protected function content_template() {
     68                ?>
     69                <li class="panel-meta customize-info accordion-section <# if ( ! data.description ) { #> cannot-expand<# } #>">
     70                        <button class="customize-panel-back" tabindex="-1" type="button"><span class="screen-reader-text"><?php _e( 'Back' ); ?></span></button>
     71                        <div class="accordion-section-title">
     72                                <span class="preview-notice"><?php
     73                                        /* translators: %s: themes panel title in the Customizer */
     74                                        echo sprintf( __( 'You are browsing %s' ), '<strong class="panel-title">' . __( 'Themes' ) . '</strong>' ); // Separate strings for consistency with other panels.
     75                                ?></span>
     76                                <?php if ( current_user_can( 'install_themes' ) && ! is_multisite() ) : ?>
     77                                        <# if ( data.description ) { #>
     78                                                <button class="customize-help-toggle dashicons dashicons-editor-help" type="button" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
     79                                        <# } #>
     80                                <?php endif; ?>
     81                        </div>
     82                        <?php if ( current_user_can( 'install_themes' ) && ! is_multisite() ) : ?>
     83                                <# if ( data.description ) { #>
     84                                        <div class="description customize-panel-description">
     85                                                {{{ data.description }}}
     86                                        </div>
     87                                <# } #>
     88                        <?php endif; ?>
     89                </li>
     90                <li id="customize-themes-loading-container">
     91                        <span class="customize-loading-text-installing-theme"><?php _e( 'Downloading your new theme&hellip;' ); ?></span>
     92                        <span class="customize-loading-text"><?php _e( 'Setting up your live preview. This may take a bit.' ); ?></span>
     93                </li><?php // Used as a full-screen overlay transition after clicking to preview a theme. ?>
     94                <li class="customize-themes-full-container-container">
     95                        <ul class="customize-themes-full-container">
     96                                <li class="customize-themes-notifications"></li>
     97                        </ul>
     98                </li>
     99                <?php
     100        }
     101}
  • src/wp-includes/customize/class-wp-customize-themes-section.php

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

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