WordPress.org

Make WordPress Core

Changeset 38813


Ignore:
Timestamp:
10/19/16 03:19:13 (12 months ago)
Author:
westonruter
Message:

Customize: Introduce a new experience for discovering, installing, and previewing themes within the customizer.

Unify the theme-browsing and theme-customization experiences by introducing a comprehensive theme browser and installer directly accessible in the customizer. Replaces the customizer theme switcher with a full-screen panel for discovering/browsing and installing themes available on WordPress.org. Themes can now be installed and previewed directly in the customizer without entering the wp-admin context.

For details, see https://make.wordpress.org/core/2016/10/03/feature-proposal-a-new-experience-for-discovering-installing-and-previewing-themes-in-the-customizer/

Fixes #37661, #34843.
Props celloexpressions, folletto, westonruter, karmatosed, afercia.

Location:
trunk
Files:
1 added
10 edited

Legend:

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

    r38709 r38813  
    272272 
    273273#customize-theme-controls .customize-pane-child.open, 
    274 #customize-theme-controls .customize-pane-child.current-panel, 
    275 #customize-theme-controls .customize-themes-panel.customize-pane-child.current-panel { 
     274#customize-theme-controls .customize-pane-child.current-panel { 
    276275    -webkit-transform: none; 
    277276    -ms-transform: none; 
     
    279278} 
    280279 
    281 #customize-theme-controls .customize-themes-panel.customize-pane-child, 
    282280.section-open #customize-theme-controls .customize-pane-parent, 
    283281.in-sub-panel #customize-theme-controls .customize-pane-parent, 
    284282.section-open #customize-info, 
    285283.in-sub-panel #customize-info, 
    286 .in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel, 
    287 .in-themes-panel #customize-theme-controls .customize-pane-parent, 
    288 .in-themes-panel #customize-info { 
     284.in-sub-panel.section-open #customize-theme-controls .customize-pane-child.current-panel { 
    289285    visibility: hidden; 
    290286    height: 0; 
     
    297293.section-open #customize-theme-controls .customize-pane-parent.busy, 
    298294.in-sub-panel #customize-theme-controls .customize-pane-parent.busy, 
    299 .in-themes-panel #customize-theme-controls .customize-pane-parent.busy, 
    300295.section-open #customize-info.busy, 
    301296.in-sub-panel #customize-info.busy, 
    302 .in-themes-panel #customize-info.busy, 
    303297.busy.section-open.in-sub-panel #customize-theme-controls .customize-pane-child.current-panel, 
    304298#customize-theme-controls .customize-pane-child.open, 
     
    308302    height: auto; 
    309303    overflow: auto; 
    310 } 
    311  
    312 .in-themes-panel #customize-theme-controls .customize-pane-parent, 
    313 .in-themes-panel #customize-info { 
    314     -webkit-transform: translateX(100%); 
    315     -ms-transform: translateX(100%); 
    316     transform: translateX(100%); 
    317304} 
    318305 
     
    407394    float: left; 
    408395    width: 48px; 
    409     height: 71px; 
     396    height: 70px; 
    410397    padding: 0 24px 0 0; 
    411398    margin: 0; 
     
    421408 
    422409.customize-section-back { 
    423     height: 74px; 
     410    height: 73px; 
    424411} 
    425412 
     
    997984} 
    998985 
    999 #customize-theme-controls .control-section-themes .accordion-section-title:hover, /* Not a focusable element. */ 
    1000 #customize-theme-controls .control-section-themes .accordion-section-title { 
     986#customize-theme-controls .control-panel-themes { 
     987    border-bottom: none; 
     988} 
     989 
     990#customize-theme-controls .control-panel-themes > .accordion-section-title:hover, /* Not a focusable element. */ 
     991#customize-theme-controls .control-panel-themes > .accordion-section-title { 
    1001992    cursor: default; 
    1002993    background: #fff; 
     
    1005996    border-bottom: 1px solid #ddd; 
    1006997    border-left: none; 
    1007     margin-top: 0; 
     998    border-right: none; 
     999    margin: 0 0 15px 0; 
     1000    padding-right: 100px; /* Space for the button */ 
    10081001} 
    10091002 
     
    10131006} 
    10141007 
    1015 #customize-theme-controls .control-section-themes > .accordion-section-title:hover, /* Not a focusable element. */ 
    1016 #customize-theme-controls .control-section-themes > .accordion-section-title { 
    1017     margin: 0 0 15px; 
    1018 } 
    1019  
    1020 #customize-controls .customize-themes-panel .accordion-section-title { 
    1021     margin: 15px -8px; 
    1022 } 
    1023  
    1024 #customize-controls .control-section-themes .accordion-section-title, 
    1025 #customize-controls .customize-themes-panel .accordion-section-title { 
    1026     padding-right: 100px; /* Space for the button */ 
    1027 } 
    1028  
    1029 #customize-controls .control-section-themes .accordion-section-title span.customize-action, 
     1008.control-panel-themes .accordion-section-title span.customize-action, 
    10301009#customize-controls .customize-section-title span.customize-action { 
    10311010    font-size: 13px; 
     
    10341013} 
    10351014 
    1036 #customize-controls .control-section-themes .accordion-section-title .change-theme, 
    1037 #customize-controls .customize-themes-panel .accordion-section-title .customize-theme { 
     1015.control-panel-themes .accordion-section-title .change-theme { 
    10381016    position: absolute; 
    10391017    right: 10px; 
     
    10431021} 
    10441022 
    1045 #customize-controls .control-section-themes .accordion-section-title:before { 
     1023#customize-theme-controls .control-panel-themes > .accordion-section-title:after { 
    10461024    display: none; 
    10471025} 
    10481026 
    1049 #customize-controls .customize-themes-panel { 
    1050     padding: 0 8px; 
    1051     background: #f1f1f1; 
    1052     -webkit-box-sizing: border-box; 
    1053     -moz-box-sizing: border-box; 
    1054     box-sizing: border-box; 
    1055 } 
    1056  
    1057 #customize-controls .customize-themes-panel .accordion-section-title:first-child { 
    1058     margin-top: 0; 
    1059 } 
    1060  
    1061 #customize-controls .customize-themes-panel .accordion-section-title:nth-child(2) { 
     1027.control-panel-themes .customize-themes-full-container { 
     1028    position: fixed; 
     1029    top: 0; 
     1030    left: 0; 
     1031    transition: .18s left ease-in-out; 
     1032    margin: 0 0 0 300px; 
     1033    padding:25px; 
     1034    overflow-y: scroll; 
     1035    width: calc(100% - 350px); 
     1036    height: calc(100% - 50px); 
     1037    background: #eee; 
     1038    z-index: 20; 
     1039} 
     1040 
     1041/* Animations for opening the themes panel */ 
     1042#customize-header-actions .save, 
     1043#customize-header-actions .spinner, 
     1044#customize-header-actions .customize-controls-preview-toggle { 
     1045    position: relative; 
     1046    top: 0; 
     1047    transition: .18s top ease-in-out; 
     1048} 
     1049 
     1050#customize-footer-actions, 
     1051#customize-footer-actions .collapse-sidebar { 
     1052    bottom: 0; 
     1053    transition: .18s bottom ease-in-out; 
     1054} 
     1055 
     1056.in-themes-panel:not(.animating) #customize-header-actions .save, 
     1057.in-themes-panel:not(.animating) #customize-header-actions .spinner, 
     1058.in-themes-panel:not(.animating) #customize-header-actions .customize-controls-preview-toggle, 
     1059.in-themes-panel:not(.animating) #customize-preview, 
     1060.in-themes-panel:not(.animating) #customize-footer-actions { 
     1061    visibility: hidden; 
     1062} 
     1063 
     1064.wp-full-overlay.in-themes-panel { 
     1065    background: #eee; /* Prevents a black flash when fading in the panel */ 
     1066} 
     1067 
     1068.in-themes-panel #customize-header-actions .save, 
     1069.in-themes-panel #customize-header-actions .spinner, 
     1070.in-themes-panel #customize-header-actions .customize-controls-preview-toggle { 
     1071    top: -45px; 
     1072} 
     1073 
     1074.in-themes-panel #customize-footer-actions, 
     1075.in-themes-panel #customize-footer-actions .collapse-sidebar { 
     1076    bottom: -45px; 
     1077} 
     1078 
     1079/* Don't show the theme count while the panel opens, as it's in the wrong place during the animation */ 
     1080.in-themes-panel.animating .control-panel-themes .filter-themes-count { 
     1081    display: none; 
     1082} 
     1083 
     1084.in-themes-panel.wp-full-overlay .wp-full-overlay-sidebar-content { 
     1085    bottom: 0; 
     1086} 
     1087 
     1088/* Adds a delay before fading in to avoid it "jumping" */ 
     1089@keyframes themes-fade-in { 
     1090    0% { 
     1091        opacity: 0; 
     1092    } 
     1093    50% { 
     1094        opacity: 0; 
     1095    } 
     1096    100% { 
     1097        opacity: 1; 
     1098    } 
     1099} 
     1100 
     1101.control-panel-themes .customize-themes-full-container.animate { 
     1102    animation: .6s themes-fade-in 1; 
     1103} 
     1104 
     1105.in-themes-panel:not(.animating) .control-panel-themes .filter-themes-count { 
     1106    animation: .6s themes-fade-in 1; 
     1107} 
     1108 
     1109.control-panel-themes .filter-themes-count { 
     1110    position: fixed; 
     1111    top: 0; 
     1112    left: 48px; 
     1113    width: 222px; 
     1114    padding: 6px 15px; 
     1115    margin: 0; 
     1116    line-height: 32px; 
     1117    text-align: right; 
     1118    z-index: 10; 
     1119} 
     1120 
     1121.control-panel-themes .filter-themes-count .themes-displayed { 
     1122    font-weight: 600; 
     1123    color: #555d66; 
     1124} 
     1125 
     1126.control-panel-themes .filter-themes-count .see-themes, 
     1127.control-panel-themes .filter-themes-count .filter-themes { 
     1128    display: none; 
     1129} 
     1130 
     1131 
     1132/* Mobile - toggle between themes and filters */ 
     1133@media screen and (max-width:600px) { 
     1134 
     1135    /* Show a spinner in the filters view also, reusing the main customize spinner */ 
     1136    .in-themes-panel.loading #customize-header-actions .spinner { 
     1137        position: fixed; 
     1138        top: 0; 
     1139        left: 48px; 
     1140        visibility: visible; 
     1141    } 
     1142 
     1143    .in-themes-panel.loading.showing-themes #customize-header-actions .spinner { 
     1144        visibility: hidden; 
     1145    } 
     1146 
     1147    .control-panel-themes .filter-themes-count { 
     1148        width: calc(100% - 93px); 
     1149    } 
     1150 
     1151    .control-panel-themes .filter-themes-count .themes-displayed { 
     1152        display: none; 
     1153    } 
     1154 
     1155    .wp-full-overlay:not(.showing-themes) .control-panel-themes .filter-themes-count .see-themes { 
     1156        display: block; 
     1157        float: right; 
     1158    } 
     1159 
     1160    .wp-full-overlay.showing-themes .control-panel-themes .filter-themes-count .filter-themes { 
     1161        display: block; 
     1162        float: right; 
     1163    } 
     1164 
     1165    .in-themes-panel.showing-themes .control-panel-themes .customize-panel-back { 
     1166        position: fixed; 
     1167        top: 0; 
     1168        left: 0; 
     1169        z-index: 10; 
     1170        height: 45px; 
     1171        background: #eee; 
     1172    } 
     1173 
     1174    .in-themes-panel.showing-themes .control-panel-themes .customize-panel-back:before { 
     1175        line-height: 45px; 
     1176    } 
     1177 
     1178    .control-panel-themes .customize-themes-full-container { 
     1179        width: calc(100% - 50px); 
     1180        margin: 0; 
     1181        top: 46px; 
     1182        height: calc(100% - 96px); 
     1183        z-index: 1; 
     1184        display: none; 
     1185    } 
     1186 
     1187    .showing-themes .control-panel-themes .customize-themes-full-container { 
     1188        display: block; 
     1189    } 
     1190} 
     1191 
     1192.control-panel-themes .customize-themes-notifications .notice { 
     1193    margin: 0 0 25px 0; 
     1194} 
     1195 
     1196.customize-themes-full-container .customize-themes-section { 
     1197    display: none !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */ 
     1198    overflow: hidden; 
     1199} 
     1200 
     1201.customize-themes-full-container .customize-themes-section.current-section { 
     1202    display: list-item !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */ 
     1203} 
     1204 
     1205.theme-section .customize-themes-text-before { 
     1206    padding: 0 0 8px 15px; 
     1207    margin: 15px 0 0 0; 
     1208    line-height: 16px; 
     1209    border-bottom: 1px solid #ddd; 
     1210    color: #555d66; 
     1211} 
     1212 
     1213.control-panel-themes .customize-themes-section-title { 
     1214    width: 100%; 
     1215    background: #fff; 
     1216    box-shadow: none; 
     1217    outline: none; 
     1218    border-top: none; 
     1219    border-bottom: 1px solid #ddd; 
     1220    border-left: 4px solid #fff; 
     1221    border-right: none; 
     1222    cursor: pointer; 
     1223    padding: 10px 15px; 
     1224    position: relative; 
     1225    text-align: left; 
    10621226    font-size: 14px; 
    10631227    font-weight: 600; 
    1064 } 
    1065  
    1066 #customize-controls .customize-themes-panel > h2 { 
    1067     padding: 15px 8px 0 8px; 
    1068 } 
    1069  
    1070 #customize-theme-controls .customize-themes-panel .accordion-section-content { 
     1228    color: #555d66; 
     1229    text-shadow: none; 
     1230} 
     1231 
     1232.control-panel-themes .theme-section { 
     1233    margin: 0; 
     1234    position: relative; 
     1235} 
     1236 
     1237.control-panel-themes .customize-themes-section-title:focus, 
     1238.control-panel-themes .customize-themes-section-title:hover { 
     1239    border-left-color: #0073aa; 
     1240    color: #0073aa; 
     1241    background: #f5f5f5; 
     1242} 
     1243 
     1244.control-panel-themes .theme-section .customize-themes-section-title.selected:after { 
     1245    content: "\f147"; 
     1246    font: 16px/1 dashicons; 
     1247    box-sizing: border-box; 
     1248    width: 20px; 
     1249    height: 20px; 
     1250    padding: 3px 3px 1px 1px; /* Re-align the icon to the smaller grid */ 
     1251    border-radius: 100%; 
     1252    position: absolute; 
     1253    top: 9px; 
     1254    right: 15px; 
     1255    background: #0073aa; 
     1256    color: #fff; 
     1257} 
     1258 
     1259.control-panel-themes .customize-themes-section-title.selected { 
     1260    color: #0073aa; 
     1261} 
     1262 
     1263.control-panel-themes .customize-themes-section-title.themes-section-search_themes { 
     1264    border-left: none; 
     1265    padding: 5px 10px 5px 15px; 
     1266    width: auto; 
     1267} 
     1268 
     1269.control-panel-themes .customize-themes-section-title.themes-section-feature_filter_themes:after, 
     1270.control-panel-themes .customize-themes-section-title.themes-section-favorites_themes:after { 
     1271    content: "\f140"; 
     1272    font: 20px/1 dashicons; 
     1273    position: absolute; 
     1274    right: 15px; 
     1275    top: 8px; 
     1276} 
     1277 
     1278.control-panel-themes .customize-themes-section-title.themes-section-search_themes .wp-filter-search { 
     1279    width: 100%; 
     1280} 
     1281 
     1282.control-panel-themes .customize-themes-section-title.themes-section-search_themes.selected, 
     1283.control-panel-themes .customize-themes-section-title.themes-section-search_themes:hover { 
     1284    background: #fff; 
     1285    cursor: default; 
     1286} 
     1287 
     1288.control-panel-themes .customize-themes-section-title.themes-section-feature_filter_themes { 
     1289    margin-top: 15px; 
     1290    border-top: 1px solid #ddd; 
     1291} 
     1292 
     1293.control-panel-themes .filter-details { 
     1294    background: #f5f5f5; 
     1295    margin: 0; 
     1296    padding: 8px 15px; 
     1297    border-top: none; 
     1298    border-bottom: 1px solid #ddd; 
     1299    display: none; 
     1300} 
     1301 
     1302.control-panel-themes .customize-themes-section-title.selected.details-open { 
     1303    border-bottom-color: #f5f5f5; 
     1304    border-left-color: #f5f5f5; 
     1305    background: #f5f5f5; 
     1306} 
     1307 
     1308.control-panel-themes .favorites-form.filter-details label { 
     1309    padding-bottom: 6px; 
     1310    display: inline-block; 
     1311} 
     1312 
     1313.control-panel-themes .filter-details .filter-group { 
     1314    float: none; 
     1315    width: 100%; 
    10711316    background: transparent; 
    1072     display: block; 
    1073 } 
    1074  
    1075 .customize-control.customize-control-theme { 
    1076     margin-bottom: 8px; 
     1317    border: none; 
     1318    padding: 0; 
     1319    box-shadow: none; 
     1320} 
     1321 
     1322.control-panel-themes .filter-details .filter-group legend button { 
     1323    padding: 18px 15px 8px 10px; 
     1324    line-height: 14px; 
     1325    border-bottom: 1px solid #ddd; 
     1326    width: 100%; 
     1327    text-align: left; 
     1328} 
     1329 
     1330.control-panel-themes .filter-details .filter-group legend { 
     1331    position: relative; 
     1332    top: 0; 
     1333    width: 100%; 
     1334} 
     1335 
     1336.control-panel-themes .filter-details .filter-group legend button:after { 
     1337    content: "\f140"; 
     1338    font: 20px/1 dashicons; 
     1339    position: absolute; 
     1340    bottom: 6px; 
     1341    right: 5px; 
     1342} 
     1343 
     1344.control-panel-themes .filter-details .filter-group legend button:hover, 
     1345.control-panel-themes .filter-details .filter-group legend button:focus { 
     1346    color: #0073aa; 
     1347    border-bottom-color: #0073aa; /* Color change for focus style should be acceptable because border-bottom is barely visible previously. */ 
     1348    outline: none; 
     1349    box-shadow: none; 
     1350} 
     1351 
     1352.control-panel-themes .filter-details .filter-group legend button.open:after { 
     1353    content: "\f142"; 
     1354} 
     1355 
     1356.control-panel-themes .filter-details .filter-group .filter-group-feature { 
     1357    display: none; 
     1358    margin: 0; 
     1359} 
     1360 
     1361.control-panel-themes .filter-details .filter-group-feature label { 
     1362    border: 1px solid #ddd; 
     1363    border-top: 0; 
     1364    background: #fff; 
     1365    color: #555d66; 
     1366    margin: 0; 
     1367    padding: 12px 10px 12px 34px; 
     1368    width: calc(100% - 46px); 
     1369    line-height: 16px; 
     1370    font-weight: 600; 
     1371} 
     1372 
     1373.control-panel-themes .filter-details .filter-group-feature input { 
     1374    position: absolute; 
     1375    margin: 12px 10px; 
     1376} 
     1377 
     1378.control-panel-themes .filter-details .filter-group-feature label:hover { 
     1379    color: #0073aa; 
    10771380} 
    10781381 
     
    10841387} 
    10851388 
     1389.loading .customize-themes-section .spinner { 
     1390    display: block; 
     1391    visibility: visible; 
     1392    position: relative; 
     1393    clear: both; 
     1394    width: 20px; 
     1395    height: 20px; 
     1396    left: calc(50% - 10px); 
     1397    float: none; 
     1398    margin-top: 50px; 
     1399} 
     1400 
     1401.customize-themes-section .filter-drawer { 
     1402    border-top: none; 
     1403    display: block; 
     1404    background: transparent; 
     1405    padding-top: 5px; 
     1406} 
     1407 
     1408.customize-themes-section .clear-filters { 
     1409    margin-left: 8px; 
     1410    display: none; 
     1411} 
     1412 
     1413.customize-themes-section .no-themes { 
     1414    display: none; 
     1415} 
     1416 
     1417.themes-section-installed_themes .theme .notice-success { 
     1418    display: none; /* Hide "installed" notice on installed themes tab. */ 
     1419} 
     1420 
     1421.control-panel-themes .theme-browser .theme .theme-actions .button-primary { 
     1422    margin: 0 0 0 8px; 
     1423} 
     1424 
     1425.customize-control-theme .theme { 
     1426    width: 100%; 
     1427    margin: 0; 
     1428} 
     1429 
     1430.customize-control.customize-control-theme { /* override most properties on .customize-control */ 
     1431    box-sizing: border-box; 
     1432    width: 18.4%; 
     1433    margin: 0 2% 2% 0; 
     1434    padding: 0; 
     1435    clear: none; 
     1436} 
     1437 
     1438/* 5 columns above 2100px */ 
     1439@media screen and (min-width: 2101px) { 
     1440    .customize-control.customize-control-theme:nth-child(5n) { 
     1441        margin-right: 0; 
     1442    } 
     1443} 
     1444 
     1445/* 4 columns up to 2100px */ 
     1446@media screen and (min-width: 1601px) and (max-width: 2100px) { 
     1447    .customize-control.customize-control-theme { 
     1448        width: 23.5%; 
     1449    } 
     1450 
     1451    .customize-control.customize-control-theme:nth-child(4n) { 
     1452        margin-right: 0; 
     1453    } 
     1454} 
     1455 
     1456/* 3 columns up to 1600px */ 
     1457@media screen and (min-width: 1201px) and (max-width: 1600px) { 
     1458    .customize-control.customize-control-theme { 
     1459        width: 32%; 
     1460    } 
     1461 
     1462    .customize-control.customize-control-theme:nth-child(3n) { 
     1463        margin-right: 0; 
     1464    } 
     1465} 
     1466 
     1467/* 2 columns up to 1200px */ 
     1468@media screen and (min-width: 851px) and (max-width: 1200px) { 
     1469    .customize-control.customize-control-theme { 
     1470        width: 49%; 
     1471    } 
     1472 
     1473    .customize-control.customize-control-theme:nth-child(even) { 
     1474        margin-right: 0; 
     1475    } 
     1476} 
     1477 
     1478/* 1 column up to 850 px */ 
     1479@media screen and (max-width: 850px) { 
     1480    .customize-control.customize-control-theme { 
     1481        width: 100%; 
     1482        margin: 0 0 3% 0; 
     1483    } 
     1484} 
     1485 
    10861486.wp-customizer .theme-browser .themes { 
    10871487    padding-bottom: 8px; 
    10881488} 
    10891489 
    1090 .wp-customizer .theme-browser .theme { 
    1091     margin: 0; 
    1092     width: 100%; 
    1093 } 
    1094  
    10951490.wp-customizer .theme-browser .theme .theme-actions { 
    1096     -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; 
    10971491    opacity: 1; 
    10981492} 
     
    11111505    line-height: 1.5; 
    11121506    width: 100%; 
    1113 } 
    1114  
    1115 .control-section-themes .accordion-section-title:after, 
    1116 .customize-themes-panel .accordion-section-title:after { 
    1117     display: none; 
    1118 } 
    1119  
    1120 .customize-themes-panel.control-panel-content { 
    1121     border-top: 1px solid #ddd; 
    11221507} 
    11231508 
     
    11361521} 
    11371522 
     1523/* Avoid a z-index war by resetting elements that should be under the overlay. 
     1524   This is likely required because of the way that sections and panels are positioned. */ 
     1525.wp-customizer.modal-open #customize-header-actions, 
     1526.wp-customizer.modal-open .control-panel-themes .filter-themes-count, 
     1527.wp-customizer.modal-open .control-panel-themes .customize-themes-section-title.selected:after { 
     1528    z-index: -1; 
     1529} 
     1530 
    11381531.wp-customizer .theme-overlay .theme-backdrop { 
    11391532    background: rgba( 238, 238, 238, 0.75 ); 
    11401533    position: fixed; 
    11411534    z-index: 110; 
     1535} 
     1536 
     1537.wp-customizer .theme-overlay .star-rating { 
     1538    float: left; 
     1539    margin-right: 8px; 
     1540} 
     1541 
     1542.wp-customizer .theme-rating .num-ratings { 
     1543    line-height: 20px; 
    11421544} 
    11431545 
     
    11481550    bottom: 45px; 
    11491551    z-index: 120; 
    1150     max-width: 1740px; /* To ensure that theme screenshots are not displayed larger than 880px wide. */ 
    11511552} 
    11521553 
    11531554.wp-customizer .theme-overlay .theme-actions { 
    1154     text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */ 
    1155 } 
    1156  
    1157 .ie8 .wp-customizer .theme-overlay .theme-header, 
    1158 .ie8 .wp-customizer .theme-overlay .theme-about, 
    1159 .ie8 .wp-customizer .theme-overlay .theme-actions { 
    1160     position: static; 
    1161 } 
     1555    text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */ 
     1556    padding: 10px 15px; 
     1557} 
     1558 
     1559.wp-customizer .theme-overlay .theme-actions .theme-install.preview { 
     1560    margin-left: 8px; 
     1561} 
     1562 
     1563.control-panel-themes .theme-actions .delete-theme { 
     1564    left: 15px; /* these override themes.css on mobile */ 
     1565    right: auto; 
     1566    bottom: auto; 
     1567    position: absolute; 
     1568} 
     1569 
     1570.modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content { 
     1571    overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */ 
     1572} 
     1573 
    11621574 
    11631575/* Small Screens */ 
  • trunk/src/wp-admin/css/themes.css

    r38795 r38813  
    571571    margin: 0 30px 0 0; 
    572572    width: 55%; 
    573     max-width: 880px; 
     573    max-width: 1200px; /* Recommended theme screenshot width, set here to avoid stretching */ 
    574574    text-align: center; 
    575575} 
  • trunk/src/wp-admin/customize.php

    r38810 r38813  
    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 
  • trunk/src/wp-admin/includes/theme.php

    r38788 r38813  
    608608 */ 
    609609function customize_themes_print_templates() { 
    610     $preview_url = esc_url( add_query_arg( 'theme', '__THEME__' ) ); // Token because esc_url() strips curly braces. 
    611     $preview_url = str_replace( '__THEME__', '{{ data.id }}', $preview_url ); 
    612610    ?> 
    613611    <script type="text/html" id="tmpl-customize-themes-details-view"> 
     
    621619            <div class="theme-about wp-clearfix"> 
    622620                <div class="theme-screenshots"> 
    623                 <# if ( data.screenshot[0] ) { #> 
     621                <# if ( data.screenshot && data.screenshot[0] ) { #> 
    624622                    <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div> 
    625623                <# } else { #> 
     
    634632                    <h2 class="theme-name">{{{ data.name }}}<span class="theme-version"><?php printf( __( 'Version: %s' ), '{{ data.version }}' ); ?></span></h2> 
    635633                    <h3 class="theme-author"><?php printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' ); ?></h3> 
     634 
     635                    <# if ( data.stars && 0 != data.num_ratings ) { #> 
     636                        <div class="theme-rating"> 
     637                            {{{ data.stars }}} 
     638                            <span class="num-ratings"><?php echo sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ); ?></span> 
     639                        </div> 
     640                    <# } #> 
     641 
     642                    <# if ( data.hasUpdate ) { #> 
     643                        <div class="notice notice-warning notice-alt notice-large" data-slug="{{ data.id }}"> 
     644                            <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3> 
     645                            {{{ data.update }}} 
     646                        </div> 
     647                    <# } #> 
     648 
    636649                    <p class="theme-description">{{{ data.description }}}</p> 
    637650 
     
    639652                        <p class="parent-theme"><?php printf( __( 'This is a child theme of %s.' ), '<strong>{{{ data.parent }}}</strong>' ); ?></p> 
    640653                    <# } #> 
    641  
    642654                    <# if ( data.tags ) { #> 
    643                         <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{ data.tags }}</p> 
     655                        <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p> 
    644656                    <# } #> 
    645657                </div> 
    646658            </div> 
    647659 
    648             <# if ( ! data.active ) { #> 
    649                 <div class="theme-actions"> 
    650                     <div class="inactive-theme"> 
    651                         <?php 
    652                         /* translators: %s: Theme name */ 
    653                         $aria_label = sprintf( __( 'Preview %s' ), '{{ data.name }}' ); 
    654                         ?> 
    655                         <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> 
    656                     </div> 
    657                 </div> 
    658             <# } #> 
     660            <div class="theme-actions"> 
     661                <# if ( data.active ) { #> 
     662                    <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></a> 
     663                <# } else if ( 'installed' === data.type ) { #> 
     664                    <?php if ( current_user_can( 'delete_themes' ) ) { ?> 
     665                        <# if ( data.actions && data.actions['delete'] ) { #> 
     666                            <a href="{{{ data.actions['delete'] }}}" data-slug="{{ data.id }}" class="button button-secondary delete-theme"><?php _e( 'Delete' ); ?></a> 
     667                        <# } #> 
     668                    <?php } ?> 
     669                    <button type="button" class="button button-primary preview-theme" data-slug="{{ data.id }}"><?php _e( 'Live Preview' ); ?></span> 
     670                <# } else { #> 
     671                    <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button> 
     672                    <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}"><?php _e( 'Install & Preview' ); ?></button> 
     673                <# } #> 
     674            </div> 
    659675        </div> 
    660676    </script> 
  • trunk/src/wp-admin/js/customize-controls.js

    r38810 r38813  
    1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */ 
     1/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console */ 
    22(function( exports, $ ){ 
    33    var Container, focus, normalizedTransitionendEventName, api = wp.customize; 
     
    865865                container = $( '#customize-theme-controls' ); 
    866866 
    867             // Watch for changes to the panel state 
     867            // Watch for changes to the panel state. 
    868868            inject = function ( panelId ) { 
    869869                var parentContainer; 
    870870                if ( panelId ) { 
    871                     // The panel has been supplied, so wait until the panel object is registered 
     871                    // The panel has been supplied, so wait until the panel object is registered. 
    872872                    api.panel( panelId, function ( panel ) { 
    873                         // The panel has been registered, wait for it to become ready/initialized 
     873                        // The panel has been registered, wait for it to become ready/initialized. 
    874874                        panel.deferred.embedded.done( function () { 
    875875                            parentContainer = panel.contentContainer; 
     
    896896            }; 
    897897            section.panel.bind( inject ); 
    898             inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one 
     898            inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one. 
    899899        }, 
    900900 
     
    10401040     * wp.customize.ThemesSection 
    10411041     * 
    1042      * Custom section for themes that functions similarly to a backwards panel, 
    1043      * and also handles the theme-details view rendering and navigation. 
     1042     * Custom section for themes that loads themes by category, and also 
     1043     * handles the theme-details view rendering and navigation. 
    10441044     * 
    10451045     * @constructor 
     
    10531053        screenshotQueue: null, 
    10541054        $window: $( window ), 
    1055  
    1056         /** 
    1057          * @since 4.2.0 
    1058          */ 
    1059         initialize: function () { 
    1060             this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' ); 
    1061             return api.Section.prototype.initialize.apply( this, arguments ); 
     1055        loaded: 0, 
     1056        loading: false, 
     1057        fullyLoaded: false, 
     1058        term: '', 
     1059        filterContainer: $(), 
     1060 
     1061        /** 
     1062         * Embed the section in the DOM when the themes panel is ready. 
     1063         * 
     1064         * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel. 
     1065         * 
     1066         * @since 4.7.0 
     1067         */ 
     1068        embed: function () { 
     1069            var inject, 
     1070                section = this, 
     1071                container = $( '#customize-theme-controls' ); 
     1072 
     1073            // Watch for changes to the panel state 
     1074            inject = function ( panelId ) { 
     1075                var parentContainer; 
     1076                api.panel( panelId, function ( panel ) { 
     1077                    // The panel has been registered, wait for it to become ready/initialized 
     1078                    panel.deferred.embedded.done( function () { 
     1079                        parentContainer = panel.contentContainer; 
     1080                        if ( ! section.headContainer.parent().is( parentContainer ) ) { 
     1081                            parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer ); 
     1082                        } 
     1083                        if ( ! section.contentContainer.parent().is( section.headContainer ) ) { 
     1084                            container.append( section.contentContainer ); 
     1085                        } 
     1086                        section.deferred.embedded.resolve(); 
     1087                    }); 
     1088                } ); 
     1089            }; 
     1090            section.panel.bind( inject ); 
     1091            inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one 
    10621092        }, 
    10631093 
     
    10921122            }); 
    10931123 
    1094             _.bindAll( this, 'renderScreenshots' ); 
     1124            _.bindAll( this, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' ); 
    10951125        }, 
    10961126 
     
    10991129         * 
    11001130         * Ignore the active states' of the contained theme controls, and just 
    1101          * use the section's own active state instead. This ensures empty search 
    1102          * results for themes to cause the section to become inactive. 
     1131         * use the section's own active state instead. This prevents empty search 
     1132         * results for theme sections from causing the section to become inactive. 
    11031133         * 
    11041134         * @since 4.2.0 
     
    11161146            var section = this; 
    11171147 
    1118             // Expand/Collapse section/panel. 
    1119             section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) { 
    1120                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { 
    1121                     return; 
    1122                 } 
    1123                 event.preventDefault(); // Keep this AFTER the key filter above 
    1124  
    1125                 if ( section.expanded() ) { 
    1126                     section.collapse(); 
    1127                 } else { 
     1148            section.filterContainer = $( '#accordion-section-' + section.id ); 
     1149 
     1150            // Expand section/panel. Only collapse when opening another section. 
     1151            section.filterContainer.on( 'click', '.customize-themes-section-title', function() { 
     1152                // Open the section. 
     1153                if ( ! section.expanded() ) { 
    11281154                    section.expand(); 
    11291155                } 
     1156 
     1157                // Toggle filters. 
     1158                if ( section.filterContainer.find( '.filter-details' ).length ) { 
     1159                    section.filterContainer.find( '.customize-themes-section-title' ) 
     1160                        .toggleClass( 'details-open' ) 
     1161                        .attr('aria-expanded', function ( i, attr ) { 
     1162                            return attr === 'true' ? 'false' : 'true'; 
     1163                        }); 
     1164                    section.filterContainer.find( '.filter-details' ).slideToggle( 180 ); 
     1165                } 
     1166            }); 
     1167 
     1168            // Preview installed themes. 
     1169            section.container.on( 'click', '.theme-actions .preview-theme', function() { 
     1170                var themeId = $( this ).data( 'themeId' ); 
     1171 
     1172                $( '.wp-full-overlay' ).addClass( 'customize-loading' ); 
     1173                api.panel( 'themes' ).loadThemePreview( themeId ).fail( function() { 
     1174                    $( '.wp-full-overlay' ).removeClass( 'customize-loading' ); 
     1175                } ); 
    11301176            }); 
    11311177 
    11321178            // Theme navigation in details view. 
    1133             section.container.on( 'click keydown', '.left', function( event ) { 
    1134                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { 
    1135                     return; 
    1136                 } 
    1137  
    1138                 event.preventDefault(); // Keep this AFTER the key filter above 
    1139  
     1179            section.container.on( 'click', '.left', function() { 
    11401180                section.previousTheme(); 
    11411181            }); 
    11421182 
    1143             section.container.on( 'click keydown', '.right', function( event ) { 
    1144                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { 
    1145                     return; 
    1146                 } 
    1147  
    1148                 event.preventDefault(); // Keep this AFTER the key filter above 
    1149  
     1183            section.container.on( 'click', '.right', function() { 
    11501184                section.nextTheme(); 
    11511185            }); 
    11521186 
    1153             section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) { 
    1154                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { 
    1155                     return; 
    1156                 } 
    1157  
    1158                 event.preventDefault(); // Keep this AFTER the key filter above 
    1159  
     1187            section.container.on( 'click', '.theme-backdrop, .close', function() { 
    11601188                section.closeDetails(); 
    11611189            }); 
    11621190 
    11631191            var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 ); 
    1164             section.container.on( 'input', '#themes-filter', function( event ) { 
     1192 
     1193            // Only used when there is only one section - installed themes. 
     1194            $( '.control-panel-themes' ).on( 'input', '#themes-filter', function( event ) { 
    11651195                var count, 
    11661196                    term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ), 
     
    11741204 
    11751205                // Update theme count. 
    1176                 count = section.container.find( 'li.customize-control:visible' ).length; 
    1177                 section.container.find( '.theme-count' ).text( count ); 
    1178             }); 
    1179  
    1180             // Pre-load the first 3 theme screenshots. 
    1181             api.bind( 'ready', function () { 
    1182                 _.each( section.controls().slice( 0, 3 ), function ( control ) { 
    1183                     var img, src = control.params.theme.screenshot[0]; 
    1184                     if ( src ) { 
    1185                         img = new Image(); 
    1186                         img.src = src; 
     1206                count = section.contentContainer.find( 'li.customize-control:visible' ).length; 
     1207                $( '.control-panel-themes' ).find( '.theme-count' ).text( count ); 
     1208            }); 
     1209 
     1210            // Event listeners for queries with user-entered terms. 
     1211            if ( 'search' === section.params.action ) { 
     1212                var debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search. 
     1213                $( '.control-panel-themes' ).on( 'input', '#wp-filter-search-input', function() { 
     1214                    debounced( section ); 
     1215                    if ( ! section.expanded() ) { 
     1216                        section.expand(); 
    11871217                    } 
    11881218                }); 
     1219 
     1220                // Focus the input if the icon is clicked. 
     1221                section.filterContainer.find( '.search-form' ).on( 'click', function( e ) { 
     1222                    if ( ! $( e.currentTarget ).hasClass( 'wp-filter-search' ) ) { 
     1223                        $( e.currentTarget ).find( '.wp-filter-search' ).focus(); 
     1224                    } 
     1225                }); 
     1226            } else if ( 'favorites' === section.params.action ) { 
     1227                section.container.on( 'click', '.favorites-form-submit', function() { 
     1228                    section.checkTerm( section ); 
     1229                }); 
     1230                section.container.on( 'keydown', '#wporg-username-input', function( e ) { 
     1231                    if ( api.utils.isKeydownButNotEnterEvent( e ) ) { 
     1232                        return; 
     1233                    } 
     1234                    section.checkTerm( section ); 
     1235                }); 
     1236            } else if ( 'feature_filter' === section.params.action ) { 
     1237                section.container.on( 'click', '.filter-group input', function() { 
     1238                    section.filtersChecked(); 
     1239                    section.checkTerm( section ); 
     1240                }); 
     1241 
     1242                // Toggle feature filter sections. 
     1243                section.container.on( 'click', '.filter-group legend button', function( e ) { 
     1244                    $( e.currentTarget ) 
     1245                        .toggleClass( 'open' ) 
     1246                        .attr('aria-expanded', function ( i, attr ) { 
     1247                            return attr === 'true' ? 'false' : 'true'; 
     1248                        }) 
     1249                        .parent().next( '.filter-group-feature' ).slideToggle( 180 ); 
     1250                }); 
     1251            } 
     1252 
     1253            // Move section controls to the themes area. 
     1254            api.bind( 'ready', function () { 
     1255                section.contentContainer = section.container.find( '.customize-themes-section' ); 
     1256                section.contentContainer.appendTo( $( '.customize-themes-full-container' ) ); 
     1257                section.container.add( section.filterContainer ); 
    11891258            }); 
    11901259        }, 
     
    11981267         * @param {Object}   args 
    11991268         * @param {Boolean}  args.unchanged 
    1200          * @param {Callback} args.completeCallback 
     1269         * @param {Function} args.completeCallback 
    12011270         */ 
    12021271        onChangeExpanded: function ( expanded, args ) { 
     
    12111280 
    12121281            // Note: there is a second argument 'args' passed 
    1213             var panel = this, 
    1214                 section = panel.contentContainer, 
    1215                 overlay = section.closest( '.wp-full-overlay' ), 
    1216                 container = section.closest( '.wp-full-overlay-sidebar-content' ), 
    1217                 customizeBtn = section.find( '.customize-theme' ), 
    1218                 changeBtn = panel.headContainer.find( '.change-theme' ); 
    1219  
    1220             if ( expanded && ! section.hasClass( 'current-panel' ) ) { 
     1282            var section = this, 
     1283                container = section.contentContainer.closest( '.customize-themes-full-container' ); 
     1284 
     1285            if ( expanded ) { 
     1286 
     1287                if ( -1 !== $.inArray( section.params.action, [ 'search', 'favorites', 'feature_filter' ] ) && '' === section.term ) { 
     1288                    section.collapse(); // Note that the current section hasn't been collapsed yet, so this is all we need to do to do nothing. 
     1289                    return; // Don't expand to an empty section that can't load any themes. 
     1290                } 
     1291 
     1292                // Load controls if none are loaded yet. 
     1293                if ( 0 === section.loaded ) { 
     1294                    section.loadControls(); 
     1295                } 
     1296 
    12211297                // Collapse any sibling sections/panels 
    12221298                api.section.each( function ( otherSection ) { 
    1223                     if ( otherSection !== panel ) { 
     1299                    if ( otherSection !== section ) { 
    12241300                        otherSection.collapse( { duration: args.duration } ); 
    12251301                    } 
    12261302                }); 
    1227                 api.panel.each( function ( otherPanel ) { 
    1228                     otherPanel.collapse( { duration: 0 } ); 
    1229                 }); 
    1230  
    1231                 panel._animateChangeExpanded( function() { 
    1232                     changeBtn.attr( 'tabindex', '-1' ); 
    1233                     customizeBtn.attr( 'tabindex', '0' ); 
    1234  
    1235                     customizeBtn.focus(); 
    1236                     section.css( 'top', '' ); 
    1237                     container.scrollTop( 0 ); 
    1238  
    1239                     if ( args.completeCallback ) { 
    1240                         args.completeCallback(); 
     1303 
     1304                section.contentContainer.addClass( 'current-section' ); 
     1305                container.scrollTop(); 
     1306                section.filterContainer.find( '.customize-themes-section-title' ).addClass( 'selected' ); 
     1307 
     1308                container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) ); 
     1309                container.on( 'scroll', _.throttle( section.loadMore, 300 ) ); 
     1310 
     1311                if ( args.completeCallback ) { 
     1312                    args.completeCallback(); 
     1313                } 
     1314                section.updateCount(); // Show this section's count. 
     1315            } else { 
     1316                section.contentContainer.removeClass( 'current-section' ); 
     1317 
     1318                // Always hide, even if they don't exist or are already hidden. 
     1319                section.filterContainer.find( '.customize-themes-section-title' ).removeClass( 'selected details-open' ).attr( 'aria-expanded', 'false' ); 
     1320                section.filterContainer.find( '.filter-details' ).slideUp( 180 ); 
     1321 
     1322                container.off( 'scroll' ); 
     1323 
     1324                if ( args.completeCallback ) { 
     1325                    args.completeCallback(); 
     1326                } 
     1327            } 
     1328        }, 
     1329 
     1330        /** 
     1331         * Return the section's content element without detachng from the parent. 
     1332         * 
     1333         * @since 4.7.0 
     1334         */ 
     1335        getContent: function() { 
     1336            return this.container.find( '.control-section-content' ); 
     1337        }, 
     1338 
     1339        /** 
     1340         * Load theme data via ajax and add themes to the section as controls. 
     1341         * 
     1342         * @since 4.7.0 
     1343         */ 
     1344        loadControls: function() { 
     1345            var section = this, params, page, request; 
     1346 
     1347            if ( section.loading ) { 
     1348                return; // We're already loading a batch of themes. 
     1349            } 
     1350 
     1351            // Parameters for every API query. Additional params are set in PHP. 
     1352            page = Math.ceil( section.loaded / 100 ) + 1; 
     1353            params = { 
     1354                'switch-themes-nonce': api.settings.nonce['switch-themes'], 
     1355                'wp_customize': 'on', 
     1356                'theme_action': section.params.action, 
     1357                'customized_theme': api.settings.theme.stylesheet, 
     1358                'page': page 
     1359            }; 
     1360 
     1361            // Add fields for special request actions. 
     1362            if ( 'search' === section.params.action ) { 
     1363                if ( '' === section.term ) { 
     1364                    return; 
     1365                } else { 
     1366                    params.search = section.term; 
     1367                } 
     1368            } else if ( 'favorites' === section.params.action ) { 
     1369                if ( '' === section.term ) { 
     1370                    return; 
     1371                } else { 
     1372                    params.user = section.term; 
     1373                } 
     1374            } else if ( 'feature_filter' === section.params.action ) { 
     1375                if ( '' === section.term ) { 
     1376                    return; 
     1377                } else { 
     1378                    params.tags = section.term; 
     1379                } 
     1380            } 
     1381 
     1382            // Load themes. 
     1383            section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' ); 
     1384            section.loading = true; 
     1385            section.container.find( '.no-themes' ).hide(); 
     1386            request = wp.ajax.post( 'customize-load-themes', params ); 
     1387            request.done(function( data ) { 
     1388                var themes = data.themes, 
     1389                    themeControl, newThemeControls; 
     1390                if ( 0 !== themes.length ) { 
     1391                    newThemeControls = []; 
     1392                    // Add controls for each theme. 
     1393                    _.each( themes, function ( theme ) { 
     1394                        var customizeId = section.params.action + '_theme_' + theme.id; 
     1395                        themeControl = new api.controlConstructor.theme( customizeId, { 
     1396                            params: { 
     1397                                type: 'theme', 
     1398                                content: '<li id="customize-control-theme-' + section.params.action + '_' + theme.id + '" class="customize-control customize-control-theme"></li>', 
     1399                                section: section.params.id, 
     1400                                active: true, 
     1401                                theme: theme, 
     1402                                priority: section.loaded + 1 
     1403                            }, 
     1404                            previewer: api.previewer 
     1405                        } ); 
     1406 
     1407                        api.control.add( customizeId, themeControl ); 
     1408                        newThemeControls.push( themeControl ); 
     1409                        section.loaded = section.loaded + 1; 
     1410                    }); 
     1411 
     1412                    if ( 1 === page ) { 
     1413                        // Pre-load the first 3 theme screenshots. 
     1414                        _.each( section.controls().slice( 0, 3 ), function ( control ) { 
     1415                            var img, src = control.params.theme.screenshot[0]; 
     1416                            if ( src ) { 
     1417                                img = new Image(); 
     1418                                img.src = src; 
     1419                            } 
     1420                        }); 
     1421                        if ( 'search' === section.params.action ) { 
     1422                            wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) ); 
     1423                        } 
     1424                    } else { 
     1425                        Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue. 
    12411426                    } 
    1242                 } ); 
    1243  
    1244                 overlay.addClass( 'in-themes-panel' ); 
    1245                 section.addClass( 'current-panel' ); 
    1246  
    1247             } else if ( ! expanded && section.hasClass( 'current-panel' ) ) { 
    1248                 panel._animateChangeExpanded( function() { 
    1249                     changeBtn.attr( 'tabindex', '0' ); 
    1250                     customizeBtn.attr( 'tabindex', '-1' ); 
    1251  
    1252                     changeBtn.focus(); 
    1253                     section.css( 'top', '' ); 
    1254  
    1255                     if ( args.completeCallback ) { 
    1256                         args.completeCallback(); 
     1427                    _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible. 
     1428 
     1429                    if ( 'installed' === section.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list. 
     1430                        section.fullyLoaded = true; 
    12571431                    } 
    1258                 } ); 
    1259  
    1260                 overlay.removeClass( 'in-themes-panel' ); 
    1261                 section.removeClass( 'current-panel' ); 
     1432                } else { 
     1433                    if ( 0 === section.loaded ) { 
     1434                        section.container.find( '.no-themes' ).show(); 
     1435                        wp.a11y.speak( section.container.find( '.no-themes' ).text() ); 
     1436                    } else { 
     1437                        section.fullyLoaded = true; 
     1438                    } 
     1439                } 
     1440                if ( 'installed' === section.params.action ) { 
     1441                    section.updateCount(); 
     1442                } else { 
     1443                    section.updateCount( data.info.results ); 
     1444                } 
     1445                section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown. 
     1446 
     1447                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case. 
     1448                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' ); 
     1449                section.loading = false; 
     1450            }); 
     1451            request.fail(function( data ) { 
     1452                if ( 'undefined' === typeof data ) { 
     1453                    section.container.find( '.unexpected-error' ).show(); 
     1454                    wp.a11y.speak( section.container.find( '.unexpected-error' ).text() ); 
     1455                } else if ( typeof console !== 'undefined' && console.error ) { 
     1456                    console.error( data ); 
     1457                } 
     1458 
     1459                // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case. 
     1460                section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' ); 
     1461                section.loading = false; 
     1462            }); 
     1463        }, 
     1464 
     1465        /** 
     1466         * Determines whether more themes should be loaded, and loads them. 
     1467         * 
     1468         * @since 4.7.0 
     1469         */ 
     1470        loadMore: function() { 
     1471            var section = this, container, bottom, threshold; 
     1472            if ( ! section.fullyLoaded && ! section.loading ) { 
     1473                container = section.container.closest( '.customize-themes-full-container' ); 
     1474 
     1475                bottom = container.scrollTop() + container.height(); 
     1476                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. 
     1477 
     1478                if ( bottom > threshold ) { 
     1479                    section.loadControls(); 
     1480                } 
     1481            } 
     1482        }, 
     1483 
     1484        /** 
     1485         * Event handler for search, feature filter, and favorites input that determines if the term has changed and loads new controls as needed. 
     1486         * 
     1487         * @since 4.7.0 
     1488         * 
     1489         * @param {wp.customize.ThemesSection} section The current theme section, passed through the debouncer. 
     1490         */ 
     1491        checkTerm: function( section ) { 
     1492            var newTerm; 
     1493 
     1494            // Find term. 
     1495            if ( 'search' === section.params.action ) { 
     1496                newTerm = $( '#wp-filter-search-input' ).val(); 
     1497            } else if ( 'favorites' === section.params.action ) { 
     1498                newTerm = $( '#wporg-username-input' ).val(); 
     1499            } else if ( 'feature_filter' === section.params.action ) { 
     1500                newTerm = section.term; // Set separately by filtersChecked(), as they're changed. 
     1501                if ( '' === newTerm ) { 
     1502                    return; 
     1503                } 
     1504            } else { 
     1505                return; 
     1506            } 
     1507 
     1508            if ( section.term === newTerm && 'feature_filter' !== section.params.action ) { 
     1509                return; 
     1510            } 
     1511 
     1512            // Clear the controls in the section. 
     1513            _.each( section.controls(), function( control ) { 
     1514                control.container.remove(); 
     1515                api.control.remove( control.id ); 
     1516            }); 
     1517            section.loaded = 0; 
     1518            section.fullyLoaded = false; 
     1519            section.screenshotQueue = null; 
     1520 
     1521            if ( '' !== newTerm ) { // Empty term should not show any results. 
     1522                // Run a new query, with loadControls handling paging, etc. 
     1523                section.term = newTerm; 
     1524                section.loadControls(); 
     1525                if ( ! section.expanded() ) { 
     1526                    section.expand(); // Expand the section if it isn't expanded. 
     1527                } 
     1528            } 
     1529        }, 
     1530 
     1531        /** 
     1532         * Check for filters checked in the feature filter list. 
     1533         * 
     1534         * @since 4.7.0 
     1535         */ 
     1536        filtersChecked: function() { 
     1537            var section = this, 
     1538                items = section.container.find( '.filter-group' ).find( ':checkbox' ), 
     1539                tags = []; 
     1540 
     1541            if ( 'feature_filter' !== section.params.action ) { 
     1542                return false; 
     1543            } 
     1544 
     1545            _.each( items.filter( ':checked' ), function( item ) { 
     1546                tags.push( $( item ).prop( 'value' ) ); 
     1547            }); 
     1548 
     1549            // When no filters are checked, restore initial state and return 
     1550            if ( tags.length === 0 ) { 
     1551                section.term = ''; 
     1552            } else { 
     1553                section.term = tags; 
    12621554            } 
    12631555        }, 
     
    12711563            var section = this; 
    12721564 
    1273             // Fill queue initially. 
    1274             if ( section.screenshotQueue === null ) { 
    1275                 section.screenshotQueue = section.controls(); 
    1276             } 
    1277  
    1278             // Are all screenshots rendered? 
     1565            // Fill queue initially, or check for more if empty. 
     1566            if ( section.screenshotQueue === null || 0 === section.screenshotQueue.length ) { 
     1567                // Add controls that haven't had their screenshots rendered. 
     1568                section.screenshotQueue = _.filter( section.controls(), function( control ) { 
     1569                    return ! control.screenshotRendered; 
     1570                }); 
     1571            } 
     1572 
     1573            // Are all screenshots rendered (for now)? 
    12791574            if ( ! section.screenshotQueue.length ) { 
    12801575                return; 
     
    13121607 
    13131608        /** 
     1609         * Update the number of themes in the section. 
     1610         * 
     1611         * @since 4.7.0 
     1612         */ 
     1613        updateCount: function ( count ) { 
     1614            if ( ! count ) { 
     1615                count = this.loaded; 
     1616            } 
     1617 
     1618            var displayed = this.container.closest( '.control-panel-content' ).find( '.themes-displayed' ), 
     1619                countEl = this.container.closest( '.control-panel-content' ).find( '.theme-count' ); 
     1620 
     1621            if ( 0 === count ) { 
     1622                countEl.text( count ); 
     1623            } else { 
     1624                // Animate the count change for emphasis. 
     1625                displayed.fadeOut( 180, function() { 
     1626                    countEl.text( count ); 
     1627                    displayed.fadeIn( 180 ); 
     1628                } ); 
     1629                wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) ); 
     1630            } 
     1631        }, 
     1632 
     1633        /** 
    13141634         * Advance the modal to the next theme. 
    13151635         * 
     
    13311651         */ 
    13321652        getNextTheme: function () { 
    1333             var control, next; 
    1334             control = api.control( 'theme_' + this.currentTheme ); 
     1653            var section = this, control, next; 
     1654            control = api.control( section.params.action + '_theme_' + this.currentTheme ); 
    13351655            next = control.container.next( 'li.customize-control-theme' ); 
    13361656            if ( ! next.length ) { 
    13371657                return false; 
    13381658            } 
    1339             next = next[0].id.replace( 'customize-control-', '' ); 
     1659            next = next[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' ); 
    13401660            control = api.control( next ); 
    13411661 
     
    13631683         */ 
    13641684        getPreviousTheme: function () { 
    1365             var control, previous; 
    1366             control = api.control( 'theme_' + this.currentTheme ); 
     1685            var section = this, control, previous; 
     1686            control = api.control( section.params.action + '_theme_' + this.currentTheme ); 
    13671687            previous = control.container.prev( 'li.customize-control-theme' ); 
    13681688            if ( ! previous.length ) { 
    13691689                return false; 
    13701690            } 
    1371             previous = previous[0].id.replace( 'customize-control-', '' ); 
     1691            previous = previous[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' ); 
    13721692            control = api.control( previous ); 
    13731693 
     
    13871707                this.overlay.find( '.left' ).addClass( 'disabled' ); 
    13881708            } 
    1389         }, 
    1390  
    1391         /** 
    1392          * Load theme preview. 
    1393          * 
    1394          * @since 4.7.0 
    1395          * 
    1396          * @param {string} themeId Theme ID. 
    1397          * @returns {jQuery.promise} Promise. 
    1398          */ 
    1399         loadThemePreview: function( themeId ) { 
    1400             var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser; 
    1401  
    1402             urlParser = document.createElement( 'a' ); 
    1403             urlParser.href = location.href; 
    1404             urlParser.search = $.param( _.extend( 
    1405                 api.utils.parseQueryString( urlParser.search.substr( 1 ) ), 
    1406                 { 
    1407                     theme: themeId, 
    1408                     changeset_uuid: api.settings.changeset.uuid 
    1409                 } 
    1410             ) ); 
    1411  
    1412             overlay = $( '.wp-full-overlay' ); 
    1413             overlay.addClass( 'customize-loading' ); 
    1414  
    1415             onceProcessingComplete = function() { 
    1416                 var request; 
    1417                 if ( api.state( 'processing' ).get() > 0 ) { 
    1418                     return; 
    1419                 } 
    1420  
    1421                 api.state( 'processing' ).unbind( onceProcessingComplete ); 
    1422  
    1423                 request = api.requestChangesetUpdate(); 
    1424                 request.done( function() { 
    1425                     $( window ).off( 'beforeunload.customize-confirm' ); 
    1426                     window.location.href = urlParser.href; 
    1427                 } ); 
    1428                 request.fail( function() { 
    1429                     overlay.removeClass( 'customize-loading' ); 
    1430                 } ); 
    1431             }; 
    1432  
    1433             if ( 0 === api.state( 'processing' ).get() ) { 
    1434                 onceProcessingComplete(); 
    1435             } else { 
    1436                 api.state( 'processing' ).bind( onceProcessingComplete ); 
    1437             } 
    1438  
    1439             return deferred.promise(); 
    14401709        }, 
    14411710 
     
    14571726            section.containFocus( section.overlay ); 
    14581727            section.updateLimits(); 
     1728            wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) ); 
    14591729 
    14601730            link = section.overlay.find( '.inactive-theme > a' ); 
    1461  
    14621731            link.on( 'click', function( event ) { 
    14631732                event.preventDefault(); 
     
    14691738                link.addClass( 'disabled' ); 
    14701739 
    1471                 section.loadThemePreview( theme.id ).fail( function() { 
     1740                api.panel( 'themes' ).loadThemePreview( theme.id ).fail( function() { 
    14721741                    link.removeClass( 'disabled' ); 
    14731742                } ); 
     
    14841753            $( 'body' ).removeClass( 'modal-open' ); 
    14851754            this.overlay.fadeOut( 'fast' ); 
    1486             api.control( 'theme_' + this.currentTheme ).focus(); 
     1755            api.control( this.params.action + '_theme_' + this.currentTheme ).container.find( '.theme' ).focus(); 
    14871756        }, 
    14881757 
     
    15641833            if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) { 
    15651834                container.append( panel.contentContainer ); 
    1566                 panel.renderContent(); 
    1567             } 
     1835            } 
     1836            panel.renderContent(); 
    15681837 
    15691838            panel.deferred.embedded.resolve(); 
     
    17532022        } 
    17542023    }); 
     2024 
     2025 
     2026    /** 
     2027     * wp.customize.ThemesPanel 
     2028     * 
     2029     * Custom section for themes that displays without the customize preview. 
     2030     * 
     2031     * @constructor 
     2032     * @augments wp.customize.Panel 
     2033     * @augments wp.customize.Container 
     2034     */ 
     2035    api.ThemesPanel = api.Panel.extend({ 
     2036        installingThemes: [], 
     2037 
     2038        /** 
     2039         * @since 4.7.0 
     2040         */ 
     2041        attachEvents: function () { 
     2042            var panel = this; 
     2043 
     2044            // Attach regular panel events. 
     2045            api.Panel.prototype.attachEvents.apply( this ); 
     2046 
     2047            // Collapse panel to customize the current theme. 
     2048            panel.contentContainer.on( 'click', '.customize-theme', function() { 
     2049                panel.collapse(); 
     2050            }); 
     2051 
     2052            // Toggle between filtering and browsing themes on mobile. 
     2053            panel.contentContainer.on( 'click', '.see-themes, .filter-themes', function() { 
     2054                $( '.wp-full-overlay' ).toggleClass( 'showing-themes' ); 
     2055            }); 
     2056 
     2057            // Install (and maybe preview) a theme. 
     2058            panel.contentContainer.on( 'click', '.theme-install', function( event ) { 
     2059                panel.installTheme( event ); 
     2060            }); 
     2061 
     2062            // Update a theme. Theme cards have the class, the details modal has the id. 
     2063            panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) { 
     2064                // #update-theme is a link. 
     2065                event.preventDefault(); 
     2066                event.stopPropagation(); 
     2067 
     2068                panel.updateTheme( event ); 
     2069            }); 
     2070 
     2071            // Delete a theme. 
     2072            panel.contentContainer.on( 'click', '.delete-theme', function( event ) { 
     2073                panel.deleteTheme( event ); 
     2074            }); 
     2075 
     2076            _.bindAll( this, 'installTheme', 'updateTheme' ); 
     2077        }, 
     2078 
     2079        /** 
     2080         * Update UI to reflect expanded state 
     2081         * 
     2082         * @since 4.7.0 
     2083         * 
     2084         * @param {Boolean}  expanded 
     2085         * @param {Object}   args 
     2086         * @param {Boolean}  args.unchanged 
     2087         * @param {Function} args.completeCallback 
     2088         */ 
     2089        onChangeExpanded: function ( expanded, args ) { 
     2090 
     2091            // Expand/collapse the panel normally. 
     2092            api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] ); 
     2093 
     2094            // Immediately call the complete callback if there were no changes 
     2095            if ( args.unchanged ) { 
     2096                if ( args.completeCallback ) { 
     2097                    args.completeCallback(); 
     2098                } 
     2099                return; 
     2100            } 
     2101 
     2102            // Note: there is a second argument 'args' passed 
     2103            var panel = this, 
     2104                overlay = panel.headContainer.closest( '.wp-full-overlay' ); 
     2105 
     2106            if ( expanded ) { 
     2107                overlay 
     2108                    .addClass( 'in-themes-panel' ).addClass( 'showing-themes' ) 
     2109                    .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' ); 
     2110 
     2111                // Automatically open the installed themes section. 
     2112                api.section( 'installed_themes' ).expand(); 
     2113            } else { 
     2114                overlay 
     2115                    .removeClass( 'in-themes-panel' ) 
     2116                    .find( '.customize-themes-full-container' ).removeClass( 'animate' ); 
     2117            } 
     2118        }, 
     2119 
     2120        /** 
     2121         * Install a theme via wp.updates. 
     2122         * 
     2123         * @since 4.7.0 
     2124         */ 
     2125        installTheme: function( event ) { 
     2126            var panel = this, preview = false, slug = $( event.target ).data( 'slug' ); 
     2127 
     2128            if ( -1 !== $.inArray( this.installingThemes, slug ) ) { 
     2129                return; // Theme is already being installed. 
     2130            } 
     2131 
     2132            wp.updates.maybeRequestFilesystemCredentials( event ); 
     2133 
     2134            $( document ).one( 'wp-theme-install-success', function( event, response ) { 
     2135                var theme = false, customizeId, themeControl; 
     2136                if ( preview ) { 
     2137 
     2138                    panel.loadThemePreview( slug ).fail( function() { 
     2139                        $( '.wp-full-overlay' ).removeClass( 'customize-loading' ); 
     2140                    } ); 
     2141 
     2142                } else { 
     2143                    api.control.each( function( control ) { 
     2144                        if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) { 
     2145                            theme = control.params.theme; // Used below to add theme control. 
     2146                            control.rerenderAsInstalled( true ); 
     2147                        } 
     2148                    }); 
     2149 
     2150                    // Don't add the same theme more than once. 
     2151                    if ( ! theme || 'undefined' !== typeof api.control( 'installed_theme_' + theme.id ) ) { 
     2152                        return; 
     2153                    } 
     2154 
     2155                    // Add theme control to installed section. 
     2156                    theme.type = 'installed'; 
     2157                    customizeId = 'installed_theme_' + theme.id; 
     2158                    themeControl = new api.controlConstructor.theme( customizeId, { 
     2159                        params: { 
     2160                            type: 'theme', 
     2161                            content: $( '<li class="customize-control customize-control-theme"></li>' ).attr( 'id', 'customize-control-theme-installed_' + theme.id ).prop( 'outerHTML' ), 
     2162                            section: 'installed_themes', 
     2163                            active: true, 
     2164                            theme: theme, 
     2165                            priority: 0 // Add all newly-installed themes to the top. 
     2166                        }, 
     2167                        previewer: api.previewer 
     2168                    } ); 
     2169 
     2170                    api.control.add( customizeId, themeControl ); 
     2171                    api.control( customizeId ).container.trigger( 'render-screenshot' ); 
     2172 
     2173                    // Close the details modal if it's open to the installed theme. 
     2174                    api.section.each( function( section ) { 
     2175                        if ( 'themes' === section.params.type ) { 
     2176                            if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere. 
     2177                                section.closeDetails(); 
     2178                            } 
     2179                        } 
     2180                    }); 
     2181                } 
     2182            } ); 
     2183 
     2184            this.installingThemes.push( $( event.target ).data( 'slug' ) ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again. 
     2185            wp.updates.installTheme( { 
     2186                slug: slug 
     2187            } ); 
     2188 
     2189            // Also preview the theme as the event is triggered on Install & Preview. 
     2190            if ( $( event.target ).hasClass( 'preview' ) ) { 
     2191                preview = true; 
     2192                $( '.wp-full-overlay' ).addClass( 'customize-loading' ); 
     2193            } 
     2194        }, 
     2195 
     2196        /** 
     2197         * Load theme preview. 
     2198         * 
     2199         * @since 4.7.0 
     2200         * 
     2201         * @param {string} themeId Theme ID. 
     2202         * @returns {jQuery.promise} Promise. 
     2203         */ 
     2204        loadThemePreview: function( themeId ) { 
     2205            var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser; 
     2206 
     2207            urlParser = document.createElement( 'a' ); 
     2208            urlParser.href = location.href; 
     2209            urlParser.search = $.param( _.extend( 
     2210                api.utils.parseQueryString( urlParser.search.substr( 1 ) ), 
     2211                { 
     2212                    theme: themeId, 
     2213                    changeset_uuid: api.settings.changeset.uuid 
     2214                } 
     2215            ) ); 
     2216 
     2217            overlay = $( '.wp-full-overlay' ); 
     2218            overlay.addClass( 'customize-loading' ); 
     2219 
     2220            onceProcessingComplete = function() { 
     2221                var request; 
     2222                if ( api.state( 'processing' ).get() > 0 ) { 
     2223                    return; 
     2224                } 
     2225 
     2226                api.state( 'processing' ).unbind( onceProcessingComplete ); 
     2227 
     2228                request = api.requestChangesetUpdate(); 
     2229                request.done( function() { 
     2230                    $( window ).off( 'beforeunload.customize-confirm' ); 
     2231                    window.location.href = urlParser.href; 
     2232                } ); 
     2233                request.fail( function() { 
     2234                    overlay.removeClass( 'customize-loading' ); 
     2235                } ); 
     2236            }; 
     2237 
     2238            if ( 0 === api.state( 'processing' ).get() ) { 
     2239                onceProcessingComplete(); 
     2240            } else { 
     2241                api.state( 'processing' ).bind( onceProcessingComplete ); 
     2242            } 
     2243 
     2244            return deferred.promise(); 
     2245        }, 
     2246 
     2247        /** 
     2248         * Update a theme via wp.updates. 
     2249         * 
     2250         * @since 4.7.0 
     2251         */ 
     2252        updateTheme: function( event ) { 
     2253            wp.updates.maybeRequestFilesystemCredentials( event ); 
     2254 
     2255            $( document ).one( 'wp-theme-update-success', function( event, response ) { 
     2256                // Rerender the control to reflect the update. 
     2257                api.control.each( function( control ) { 
     2258                    if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) { 
     2259                        control.params.theme.hasUpdate = false; 
     2260                        control.rerenderAsInstalled( true ); 
     2261                    } 
     2262                }); 
     2263            } ); 
     2264 
     2265            wp.updates.updateTheme( { 
     2266                slug: $( event.target ).closest( '.notice' ).data( 'slug' ) 
     2267            } ); 
     2268        }, 
     2269 
     2270        /** 
     2271         * Delete a theme via wp.updates. 
     2272         * 
     2273         * @since 4.7.0 
     2274         */ 
     2275        deleteTheme: function( event ) { 
     2276            var theme, section; 
     2277            theme = $( event.target ).data( 'slug' ); 
     2278            section = api.section( 'installed_themes' ); 
     2279 
     2280            event.preventDefault(); 
     2281 
     2282            // Confirmation dialog for deleting a theme. 
     2283            if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) { 
     2284                return; 
     2285            } 
     2286 
     2287            wp.updates.maybeRequestFilesystemCredentials( event ); 
     2288 
     2289            $( document ).one( 'wp-theme-delete-success', function() { 
     2290                var control = api.control( 'installed_theme_' + theme ); 
     2291 
     2292                // Remove theme control. 
     2293                control.container.remove(); 
     2294                api.control.remove( control.id ); 
     2295 
     2296                // Update installed count. 
     2297                section.loaded = section.loaded - 1; 
     2298                section.updateCount(); 
     2299 
     2300                // Rerender any other theme controls as uninstalled. 
     2301                api.control.each( function( control ) { 
     2302                    if ( 'theme' === control.params.type && control.params.theme.id === theme ) { 
     2303                        control.rerenderAsInstalled( false ); 
     2304                    } 
     2305                }); 
     2306            } ); 
     2307 
     2308            wp.updates.deleteTheme( { 
     2309                slug: theme 
     2310            } ); 
     2311 
     2312            // Close modal and focus the section. 
     2313            section.closeDetails(); 
     2314            section.focus(); 
     2315        } 
     2316 
     2317    }); 
     2318 
    17552319 
    17562320    /** 
     
    20492613         * @param {Object}   args 
    20502614         * @param {Number}   args.duration 
    2051          * @param {Callback} args.completeCallback 
     2615         * @param {Function} args.completeCallback 
    20522616         */ 
    20532617        onChangeActive: function ( active, args ) { 
     
    30723636 
    30733637        touchDrag: false, 
    3074         isRendered: false, 
    3075  
    3076         /** 
    3077          * Defer rendering the theme control until the section is displayed. 
    3078          * 
    3079          * @since 4.2.0 
    3080          */ 
    3081         renderContent: function () { 
    3082             var control = this, 
    3083                 renderContentArgs = arguments; 
    3084  
    3085             api.section( control.section(), function( section ) { 
    3086                 if ( section.expanded() ) { 
    3087                     api.Control.prototype.renderContent.apply( control, renderContentArgs ); 
    3088                     control.isRendered = true; 
    3089                 } else { 
    3090                     section.expanded.bind( function( expanded ) { 
    3091                         if ( expanded && ! control.isRendered ) { 
    3092                             api.Control.prototype.renderContent.apply( control, renderContentArgs ); 
    3093                             control.isRendered = true; 
    3094                         } 
    3095                     } ); 
    3096                 } 
    3097             } ); 
    3098         }, 
     3638        screenshotRendered: false, 
    30993639 
    31003640        /** 
     
    31203660 
    31213661                // Prevent the modal from showing when the user clicks the action button. 
    3122                 if ( $( event.target ).is( '.theme-actions .button' ) ) { 
     3662                if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) { 
    31233663                    return; 
    31243664                } 
    31253665 
    3126                 api.section( control.section() ).loadThemePreview( control.params.theme.id ); 
    3127             }); 
    3128  
    3129             control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) { 
    3130                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { 
    3131                     return; 
    3132                 } 
    3133  
    31343666                event.preventDefault(); // Keep this AFTER the key filter above 
    3135  
    31363667                api.section( control.section() ).showDetails( control.params.theme ); 
    31373668            }); 
     
    31443675                    $screenshot.attr( 'src', source ); 
    31453676                } 
    3146             }); 
    3147         }, 
    3148  
    3149         /** 
    3150          * Show or hide the theme based on the presence of the term in the title, description, and author. 
     3677                control.screenshotRendered = true; 
     3678            }); 
     3679        }, 
     3680 
     3681        /** 
     3682         * Show or hide the theme based on the presence of the term in the title, description, tags, and author. 
    31513683         * 
    31523684         * @since 4.2.0 
     
    31643696                control.deactivate(); 
    31653697            } 
     3698        }, 
     3699 
     3700        /** 
     3701         * Rerender the theme from its JS template with the installed type. 
     3702         * 
     3703         * @since 4.7.0 
     3704         */ 
     3705        rerenderAsInstalled: function( installed ) { 
     3706            var control = this, section; 
     3707            if ( installed ) { 
     3708                control.params.theme.type = 'installed'; 
     3709            } else { 
     3710                section = api.section( control.params.section ); 
     3711                control.params.theme.type = section.params.action; 
     3712            } 
     3713            control.renderContent(); // replaces existing content 
     3714            control.container.trigger( 'render-screenshot' ); 
    31663715        } 
    31673716    }); 
     
    38454394        theme:         api.ThemeControl 
    38464395    }; 
    3847     api.panelConstructor = {}; 
     4396    api.panelConstructor = { 
     4397        themes: api.ThemesPanel 
     4398    }; 
    38484399    api.sectionConstructor = { 
    38494400        themes: api.ThemesSection 
     
    39634514        // Sort the sections within each panel 
    39644515        api.panel.each( function ( panel ) { 
     4516            if ( 'themes' === panel.id ) { 
     4517                return; // Don't reflow theme sections, as doing so moves them after the themes container. 
     4518            } 
     4519             
    39654520            var sections = panel.sections(), 
    39664521                sectionHeadContainers = _.pluck( sections, 'headContainer' ); 
     
    45685123            collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0]; 
    45695124            if ( collapsedObject ) { 
     5125                if ( 'themes' === collapsedObject.params.type ) { 
     5126                    // Themes panel or section. 
     5127                    if ( $( 'body' ).hasClass( 'modal-open' ) ) { 
     5128                        collapsedObject.closeDetails(); 
     5129                    } else { 
     5130                        // If we're collapsing a section, collapse the panel also. 
     5131                        wp.customize.panel( 'themes' ).collapse(); 
     5132                    } 
     5133                    return; 
     5134                } 
    45705135                collapsedObject.collapse(); 
    45715136                event.preventDefault(); 
  • trunk/src/wp-admin/js/updates.js

    r38704 r38813  
    180180            $notice.replaceWith( $adminNotice ); 
    181181        } else { 
    182             $( '.wrap' ).find( '> h1' ).after( $adminNotice ); 
     182            if ( 'customize' === pagenow ) { 
     183                $( '.customize-themes-notifications' ).append( $adminNotice ); 
     184            } else { 
     185                $( '.wrap' ).find( '> h1' ).after( $adminNotice ); 
     186            } 
    183187        } 
    184188 
     
    908912            $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); 
    909913 
     914        } else if ( 'customize' === pagenow ) { 
     915 
     916            // Update the theme details UI. 
     917            $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' ); 
     918 
     919            $notice.find( 'h3' ).remove(); 
     920 
     921            // Add the top-level UI, and update both. 
     922            $notice = $notice.add( $( '#customize-control-theme-installed_' + args.slug ).find( '.update-message' ) ); 
     923            $notice = $notice.addClass( 'updating-message' ).find( 'p' ); 
     924 
    910925        } else { 
    911926            $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' ); 
     
    950965            $notice, newText; 
    951966 
     967        if ( 'customize' === pagenow ) { 
     968            $theme = wp.customize.control( 'installed_theme_' + response.slug ).container; 
     969        } 
     970 
    952971        if ( 'themes-network' === pagenow ) { 
    953972            $notice = $theme.find( '.update-message' ); 
     
    10021021        if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) { 
    10031022            return; 
     1023        } 
     1024 
     1025        if ( 'customize' === pagenow ) { 
     1026            $theme = wp.customize.control( 'installed_theme_' + response.slug ).container; 
    10041027        } 
    10051028 
     
    11401163        } 
    11411164 
    1142         if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) { 
    1143             $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 
    1144             $card   = $( '.install-theme-info' ).prepend( $message ); 
     1165        if ( 'customize' === pagenow ) { 
     1166            if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) { 
     1167                $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 
     1168                $card   = $( '.theme-overlay .theme-info' ).prepend( $message ); 
     1169            } else { 
     1170                $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 
     1171                $card   = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message ); 
     1172            } 
     1173            $( '.wp-full-overlay' ).removeClass( 'customize-loading' ); 
    11451174        } else { 
    1146             $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message ); 
    1147             $button = $card.find( '.theme-install' ); 
     1175            if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) { 
     1176                $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 
     1177                $card   = $( '.install-theme-info' ).prepend( $message ); 
     1178            } else { 
     1179                $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message ); 
     1180                $button = $card.find( '.theme-install' ); 
     1181            } 
    11481182        } 
    11491183 
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r38810 r38813  
    295295        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' ); 
    296296 
     297        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' ); 
    297298        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' ); 
    298299        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' ); 
     
    349350        add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) ); 
    350351        add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) ); 
     352        add_action( 'wp_ajax_customize-load-themes',    array( $this, 'load_themes_ajax' ) ); 
    351353 
    352354        add_action( 'customize_register',                 array( $this, 'register_controls' ) ); 
     
    362364        // Export the settings to JS via the _wpCustomizeSettings variable. 
    363365        add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 ); 
     366 
     367        // Add theme update notices. 
     368        if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) { 
     369            require_once( ABSPATH . '/wp-admin/includes/update.php' ); 
     370            add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' ); 
     371        } 
    364372    } 
    365373 
     
    25852593            $control->enqueue(); 
    25862594        } 
     2595        if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) { 
     2596            wp_enqueue_script( 'updates' ); 
     2597        } 
    25872598    } 
    25882599 
     
    27992810            'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ), 
    28002811            'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ), 
     2812            'switch-themes' => wp_create_nonce( 'switch-themes' ), 
    28012813        ); 
    28022814 
     
    28722884            'documentTitleTmpl' => $this->get_document_title_template(), 
    28732885            'previewableDevices' => $this->get_previewable_devices(), 
     2886            'l10n' => array( 
     2887                'confirmDeleteTheme' => __( 'Are you sure you want to delete this theme?' ), 
     2888                /* translators: %d is the number of theme search results, which cannot consider singular vs. plural forms */ 
     2889                'themeSearchResults' => __( '%d themes found' ), 
     2890                /* translators: %d is the number of themes being displayed, which cannot consider singular vs. plural forms */ 
     2891                'announceThemeCount' => __( 'Displaying %d themes' ), 
     2892                'announceThemeDetails' => __( 'Showing details for theme: %s' ), 
     2893            ), 
    28742894        ); 
    28752895 
     
    29752995        /* Panel, Section, and Control Types */ 
    29762996        $this->register_panel_type( 'WP_Customize_Panel' ); 
     2997        $this->register_panel_type( 'WP_Customize_Themes_Panel' ); 
    29772998        $this->register_section_type( 'WP_Customize_Section' ); 
    29782999        $this->register_section_type( 'WP_Customize_Sidebar_Section' ); 
     3000        $this->register_section_type( 'WP_Customize_Themes_Section' ); 
    29793001        $this->register_control_type( 'WP_Customize_Color_Control' ); 
    29803002        $this->register_control_type( 'WP_Customize_Media_Control' ); 
     
    29863008        $this->register_control_type( 'WP_Customize_Theme_Control' ); 
    29873009 
    2988         /* Themes */ 
    2989  
    2990         $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array( 
    2991             'title'      => $this->theme()->display( 'Name' ), 
    2992             'capability' => 'switch_themes', 
    2993             'priority'   => 0, 
     3010        /* Themes (controls are loaded via ajax) */ 
     3011 
     3012        $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array( 
     3013            'title'       => $this->theme()->display( 'Name' ), 
     3014            '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.' ), 
     3015            'capability'  => 'switch_themes', 
     3016            'priority'    => 0, 
     3017        ) ) ); 
     3018 
     3019        $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array( 
     3020            'title'       => __( 'Installed' ), 
     3021            'text_before' => __( 'Your local site' ), 
     3022            'action'      => 'installed', 
     3023            'capability'  => 'switch_themes', 
     3024            'panel'       => 'themes', 
     3025            'priority'    => 0, 
     3026        ) ) ); 
     3027 
     3028        $this->add_section( new WP_Customize_Themes_Section( $this, 'search_themes', array( 
     3029            'title'       => __( 'Search themes&hellip;' ), 
     3030            'text_before' => __( 'Browse all WordPress.org themes' ), 
     3031            'action'      => 'search', 
     3032            'capability'  => 'install_themes', 
     3033            'panel'       => 'themes', 
     3034            'priority'    => 5, 
     3035        ) ) ); 
     3036 
     3037        $this->add_section( new WP_Customize_Themes_Section( $this, 'featured_themes', array( 
     3038            'title'      => __( 'Featured' ), 
     3039            'action'     => 'featured', 
     3040            'capability' => 'install_themes', 
     3041            'panel'      => 'themes', 
     3042            'priority'   => 10, 
     3043        ) ) ); 
     3044 
     3045        $this->add_section( new WP_Customize_Themes_Section( $this, 'popular_themes', array( 
     3046            'title'      => __( 'Popular' ), 
     3047            'action'     => 'popular', 
     3048            'capability' => 'install_themes', 
     3049            'panel'      => 'themes', 
     3050            'priority'   => 15, 
     3051        ) ) ); 
     3052 
     3053        $this->add_section( new WP_Customize_Themes_Section( $this, 'latest_themes', array( 
     3054            'title'      => __( 'Latest' ), 
     3055            'action'     => 'latest', 
     3056            'capability' => 'install_themes', 
     3057            'panel'      => 'themes', 
     3058            'priority'   => 20, 
     3059        ) ) ); 
     3060 
     3061        $this->add_section( new WP_Customize_Themes_Section( $this, 'feature_filter_themes', array( 
     3062            'title'      => __( 'Feature Filter' ), 
     3063            'action'     => 'feature_filter', 
     3064            'capability' => 'install_themes', 
     3065            'panel'      => 'themes', 
     3066            'priority'   => 25, 
     3067        ) ) ); 
     3068 
     3069        $this->add_section( new WP_Customize_Themes_Section( $this, 'favorites_themes', array( 
     3070            'title'      => __( 'Favorites' ), 
     3071            'action'     => 'favorites', 
     3072            'capability' => 'install_themes', 
     3073            'panel'      => 'themes', 
     3074            'priority'   => 30, 
    29943075        ) ) ); 
    29953076 
     
    29983079            'capability' => 'switch_themes', 
    29993080        ) ) ); 
    3000  
    3001         require_once( ABSPATH . 'wp-admin/includes/theme.php' ); 
    3002  
    3003         // Theme Controls. 
    3004  
    3005         // Add a control for the active/original theme. 
    3006         if ( ! $this->is_theme_active() ) { 
    3007             $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) ); 
    3008             $active_theme = current( $themes ); 
    3009             $active_theme['isActiveTheme'] = true; 
    3010             $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array( 
    3011                 'theme'    => $active_theme, 
    3012                 'section'  => 'themes', 
    3013                 'settings' => 'active_theme', 
    3014             ) ) ); 
    3015         } 
    3016  
    3017         $themes = wp_prepare_themes_for_js(); 
    3018         foreach ( $themes as $theme ) { 
    3019             if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) { 
    3020                 continue; 
    3021             } 
    3022  
    3023             $theme_id = 'theme_' . $theme['id']; 
    3024             $theme['isActiveTheme'] = false; 
    3025             $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array( 
    3026                 'theme'    => $theme, 
    3027                 'section'  => 'themes', 
    3028                 'settings' => 'active_theme', 
    3029             ) ) ); 
    3030         } 
    30313081 
    30323082        /* Site Identity */ 
     
    33573407 
    33583408    /** 
     3409     * Load themes into the theme browsing/installation UI. 
     3410     * 
     3411     * @since 4.7.0 
     3412     * @access public 
     3413     */ 
     3414    public function load_themes_ajax() { 
     3415        check_ajax_referer( 'switch-themes', 'switch-themes-nonce' ); 
     3416 
     3417        if ( ! current_user_can( 'switch_themes' ) ) { 
     3418            wp_die( -1 ); 
     3419        } 
     3420 
     3421        if ( empty( $_POST['theme_action'] ) ) { 
     3422            wp_send_json_error( 'missing_theme_action' ); 
     3423        } 
     3424 
     3425        if ( 'search' === $_POST['theme_action'] && ! array_key_exists( 'search', $_POST ) ) { 
     3426            wp_send_json_error( 'empty_search' ); 
     3427        } elseif ( 'favorites' === $_POST['theme_action'] && ! array_key_exists( 'user', $_POST ) ) { 
     3428            wp_send_json_error( 'empty_user' ); 
     3429        } elseif ( 'feature_filter' === $_POST['theme_action'] && ! array_key_exists( 'tags', $_POST ) ) { 
     3430            wp_send_json_error( 'no_features' ); 
     3431        } 
     3432 
     3433        require_once( ABSPATH . 'wp-admin/includes/theme.php' ); 
     3434        if ( 'installed' === $_POST['theme_action'] ) { 
     3435            $themes = array( 'themes' => wp_prepare_themes_for_js() ); 
     3436            foreach ( $themes['themes'] as &$theme ) { 
     3437                $theme['type'] = 'installed'; 
     3438                // Set active based on customized theme. 
     3439                if ( $_POST['customized_theme'] === $theme['id'] ) { 
     3440                    $theme['active'] = true; 
     3441                } else { 
     3442                    $theme['active'] = false; 
     3443                } 
     3444            } 
     3445        } else { 
     3446            if ( ! current_user_can( 'install_themes' ) ) { 
     3447                wp_die( -1 ); 
     3448            } 
     3449 
     3450            // Arguments for all queries. 
     3451            $args = array( 
     3452                'per_page' => 100, 
     3453                'page' => absint( $_POST['page'] ), 
     3454                'fields' => array( 
     3455                    'slug' => true, 
     3456                    'screenshot' => true, 
     3457                    'description' => true, 
     3458                    'requires' => true, 
     3459                    'rating' => true, 
     3460                    'downloaded' => true, 
     3461                    'downloadLink' => true, 
     3462                    'last_updated' => true, 
     3463                    'homepage' => true, 
     3464                    'num_ratings' => true, 
     3465                    'tags' => true, 
     3466                ), 
     3467            ); 
     3468 
     3469            // Specialized handling for each query. 
     3470            switch ( $_POST['theme_action'] ) { 
     3471                case 'search': 
     3472                    $args['search'] = wp_unslash( $_POST['search'] ); 
     3473                    break; 
     3474                case 'favorites': 
     3475                    $args['user'] = wp_unslash( $_POST['user'] ); 
     3476                case 'featured': 
     3477                case 'popular': 
     3478                    $args['browse'] = wp_unslash( $_POST['theme_action'] ); 
     3479                    break; 
     3480                case 'latest': 
     3481                    $args['browse'] = 'new'; 
     3482                    break; 
     3483                case 'feature_filter': 
     3484                    $args['tag'] = wp_unslash( $_POST['tags'] ); 
     3485                    break; 
     3486            } 
     3487 
     3488            // Load themes from the .org API. 
     3489            $themes = themes_api( 'query_themes', $args ); 
     3490            if ( is_wp_error( $themes ) ) { 
     3491                wp_send_json_error(); 
     3492            } 
     3493 
     3494            // This list matches the allowed tags in wp-admin/includes/theme-install.php. 
     3495            $themes_allowedtags = array('a' => array('href' => array(), 'title' => array(), 'target' => array()), 
     3496                'abbr' => array('title' => array()), 'acronym' => array('title' => array()), 
     3497                'code' => array(), 'pre' => array(), 'em' => array(), 'strong' => array(), 
     3498                'div' => array(), 'p' => array(), 'ul' => array(), 'ol' => array(), 'li' => array(), 
     3499                'h1' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'h5' => array(), 'h6' => array(), 
     3500                'img' => array('src' => array(), 'class' => array(), 'alt' => array()) 
     3501            ); 
     3502 
     3503            // Prepare a list of installed themes to check against before the loop. 
     3504            $installed_themes = array(); 
     3505            $wp_themes = wp_get_themes(); 
     3506            foreach ( $wp_themes as $theme ) { 
     3507                $installed_themes[] = $theme->get_stylesheet(); 
     3508            } 
     3509            $update_php = network_admin_url( 'update.php?action=install-theme' ); 
     3510            foreach ( $themes->themes as &$theme ) { 
     3511                $theme->install_url = add_query_arg( array( 
     3512                    'theme'    => $theme->slug, 
     3513                    '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ), 
     3514                ), $update_php ); 
     3515 
     3516                $theme->name        = wp_kses( $theme->name, $themes_allowedtags ); 
     3517                $theme->author      = wp_kses( $theme->author, $themes_allowedtags ); 
     3518                $theme->version     = wp_kses( $theme->version, $themes_allowedtags ); 
     3519                $theme->description = wp_kses( $theme->description, $themes_allowedtags ); 
     3520                $theme->tags        = implode( ', ', $theme->tags ); 
     3521                $theme->stars       = wp_star_rating( array( 'rating' => $theme->rating, 'type' => 'percent', 'number' => $theme->num_ratings, 'echo' => false ) ); 
     3522                $theme->num_ratings = number_format_i18n( $theme->num_ratings ); 
     3523                $theme->preview_url = set_url_scheme( $theme->preview_url ); 
     3524 
     3525                // Handle themes that are already installed as installed themes. 
     3526                if ( in_array( $theme->slug, $installed_themes, true ) ) { 
     3527                    $theme->type = 'installed'; 
     3528                } else { 
     3529                    $theme->type = $_POST['theme_action']; 
     3530                } 
     3531 
     3532                // Set active based on customized theme. 
     3533                if ( $_POST['customized_theme'] === $theme->slug ) { 
     3534                    $theme->active = true; 
     3535                } else { 
     3536                    $theme->active = false; 
     3537                } 
     3538 
     3539                // Map available theme properties to installed theme properties. 
     3540                $theme->id           = $theme->slug; 
     3541                $theme->screenshot   = array( $theme->screenshot_url ); 
     3542                $theme->authorAndUri = $theme->author; 
     3543                unset( $theme->slug ); 
     3544                unset( $theme->screenshot_url ); 
     3545                unset( $theme->author ); 
     3546            } // End foreach(). 
     3547        } // End if(). 
     3548        wp_send_json_success( $themes ); 
     3549    } 
     3550 
     3551 
     3552    /** 
    33593553     * Callback for validating the header_textcolor value. 
    33603554     * 
  • trunk/src/wp-includes/customize/class-wp-customize-theme-control.php

    r38810 r38813  
    6363     */ 
    6464    public function content_template() { 
    65         $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 
    66         $active_url  = esc_url( remove_query_arg( 'customize_theme', $current_url ) ); 
    67         $preview_url = esc_url( add_query_arg( 'customize_theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces. 
    68         $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url ); 
     65        /* translators: %s: theme name */ 
     66        $details_label = sprintf( __( 'Details for theme: %s' ), '{{ data.theme.name }}' ); 
     67        /* translators: %s: theme name */ 
     68        $customize_label = sprintf( __( 'Customize theme: %s' ), '{{ data.theme.name }}' ); 
     69        /* translators: %s: theme name */ 
     70        $preview_label = sprintf( __( 'Live preview theme: %s' ), '{{ data.theme.name }}' ); 
     71        /* translators: %s: theme name */ 
     72        $install_label = sprintf( __( 'Install and preview theme: %s' ), '{{ data.theme.name }}' ); 
    6973        ?> 
    70         <# if ( data.theme.isActiveTheme ) { #> 
    71             <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"> 
     74        <# if ( data.theme.active ) { #> 
     75            <div class="theme active" tabindex="0" aria-describedby="{{ data.section }}-{{ data.theme.id }}-action {{ data.theme.id }}-name"> 
    7276        <# } else { #> 
    73             <div class="theme" tabindex="0" data-preview-url="<?php echo esc_attr( $preview_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name"> 
     77            <div class="theme" tabindex="0" aria-describedby="{{ data.section }}-{{ data.theme.id }}-action {{ data.theme.id }}-name"> 
    7478        <# } #> 
    7579 
    76             <# if ( data.theme.screenshot[0] ) { #> 
     80            <# if ( data.theme.screenshot && data.theme.screenshot[0] ) { #> 
    7781                <div class="theme-screenshot"> 
    7882                    <img data-src="{{ data.theme.screenshot[0] }}" alt="" /> 
     
    8286            <# } #> 
    8387 
    84             <# if ( data.theme.isActiveTheme ) { #> 
    85                 <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Customize' ); ?></span> 
    86             <# } else { #> 
    87                 <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Live Preview' ); ?></span> 
     88            <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> 
     89 
     90 
     91            <# if ( 'installed' === data.theme.type && data.theme.hasUpdate ) { #> 
     92                <div class="update-message notice inline notice-warning notice-alt" data-slug="{{ data.theme.id }}"><p><?php printf( __( 'New version available. %s' ), '<button class="button-link update-theme" type="button">' . __( 'Update now' ) . '</button>' ); ?></p></div> 
    8893            <# } #> 
    8994 
    90             <div class="theme-author"><?php printf( __( 'By %s' ), '{{ data.theme.author }}' ); ?></div> 
    91  
    92             <# if ( data.theme.isActiveTheme ) { #> 
    93                 <h3 class="theme-name" id="{{ data.theme.id }}-name"> 
     95            <# if ( data.theme.active ) { #> 
     96                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name"> 
    9497                    <?php 
    9598                    /* translators: %s: theme name */ 
    96                     printf( __( '<span>Active:</span> %s' ), '{{{ data.theme.name }}}' ); 
     99                    printf( __( '<span>Current:</span> %s' ), '{{ data.theme.name }}' ); 
    97100                    ?> 
    98101                </h3> 
     102                <div class="theme-actions"> 
     103                    <button type="button" class="button button-primary customize-theme" aria-label="<?php echo esc_attr( $customize_label ); ?>"><?php _e( 'Customize' ); ?></button> 
     104                </div> 
     105                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div> 
     106            <# } else if ( 'installed' === data.theme.type ) { #> 
     107                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3> 
     108                <div class="theme-actions"> 
     109                    <button type="button" class="button button-primary preview-theme" aria-label="<?php echo esc_attr( $preview_label ); ?>" data-theme-id="{{ data.theme.id }}"><?php _e( 'Live Preview' ); ?></span> 
     110                </div> 
     111                <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div> 
    99112            <# } else { #> 
    100                 <h3 class="theme-name" id="{{ data.theme.id }}-name">{{{ data.theme.name }}}</h3> 
     113                <h3 class="theme-name" id="{{ data.section }}-{{ data.theme.id }}-name">{{ data.theme.name }}</h3> 
    101114                <div class="theme-actions"> 
    102                     <button type="button" class="button theme-details"><?php _e( 'Theme Details' ); ?></button> 
     115                    <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 }}" data-theme-id="{{ data.theme.id }}"><?php _e( 'Install & Preview' ); ?></button> 
    103116                </div> 
    104117            <# } #> 
  • trunk/src/wp-includes/customize/class-wp-customize-themes-section.php

    r35943 r38813  
    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 in tabbed sections. 
    1414 * 
    1515 * @since 4.2.0 
     
    2929 
    3030    /** 
    31      * Render the themes section, which behaves like a panel. 
     31     * Theme section action. 
    3232     * 
    33      * @since 4.2.0 
     33     * Defines the type of themes to load (installed, featured, latest, etc.). 
     34     * 
     35     * @since 4.7.0 
     36     * @access public 
     37     * @var string 
     38     */ 
     39    public $action = ''; 
     40 
     41    /** 
     42     * Text before theme section heading. 
     43     * 
     44     * @since 4.7.0 
     45     * @access public 
     46     * @var string 
     47     */ 
     48    public $text_before = ''; 
     49 
     50    /** 
     51     * Get section parameters for JS. 
     52     * 
     53     * @since 4.7.0 
     54     * @access public 
     55     * @return array Exported parameters. 
     56     */ 
     57    public function json() { 
     58        $exported = parent::json(); 
     59        $exported['action'] = $this->action; 
     60        $exported['text_before'] = $this->text_before; 
     61 
     62        return $exported; 
     63    } 
     64 
     65    /** 
     66     * Render a themes section as a JS template. 
     67     * 
     68     * The template is only rendered by PHP once, so all actions are prepared at once on the server side. 
     69     * 
     70     * @since 4.7.0 
    3471     * @access protected 
    3572     */ 
    36     protected function render() { 
    37         $classes = 'accordion-section control-section control-section-' . $this->type; 
     73    protected function render_template() { 
    3874        ?> 
    39         <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>"> 
    40             <h3 class="accordion-section-title"> 
    41                 <?php 
    42                 if ( $this->manager->is_theme_active() ) { 
    43                     echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title; 
     75        <li id="accordion-section-{{ data.id }}" class="theme-section"> 
     76            <# if ( '' !== data.text_before ) { #> 
     77                <p class="customize-themes-text-before">{{ data.text_before }}</p> 
     78            <# } #> 
     79            <# if ( 'search' === data.action ) { #> 
     80                <div class="search-form customize-themes-section-title themes-section-search_themes"> 
     81                    <label class="screen-reader-text" for="wp-filter-search-input">{{ data.title }}</label> 
     82                    <input placeholder="{{ data.title }}" type="text" aria-describedby="live-search-desc" id="wp-filter-search-input" class="wp-filter-search"> 
     83                    <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span> 
     84                </div> 
     85            <# } else { #> 
     86                <# if ( 'favorites' === data.action || 'feature_filter' === data.action ) { 
     87                    var attr = ' aria-expanded="false"'; 
    4488                } else { 
    45                     echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title; 
    46                 } 
    47                 ?> 
    48  
    49                 <?php if ( count( $this->controls ) > 0 ) : ?> 
    50                     <button type="button" class="button change-theme" tabindex="0"><?php _ex( 'Change', 'theme' ); ?></button> 
    51                 <?php endif; ?> 
    52             </h3> 
    53             <div class="customize-themes-panel control-panel-content themes-php"> 
    54                 <h3 class="accordion-section-title customize-section-title"> 
    55                     <span class="customize-action"><?php _e( 'Customizing' ); ?></span> 
    56                     <?php _e( 'Themes' ); ?> 
    57                     <span class="title-count theme-count"><?php echo count( $this->controls ) + 1 /* Active theme */; ?></span> 
    58                 </h3> 
    59                 <h3 class="accordion-section-title customize-section-title"> 
     89                    var attr = ''; 
     90                } #> 
     91                <button type="button" class="customize-themes-section-title themes-section-{{ data.id }}"{{{ attr }}}>{{ data.title }}</button> 
     92            <# } #> 
     93            <?php if ( ! current_user_can( 'install_themes' ) || is_multisite() ) : ?> 
     94                <# if ( 'installed' === data.action ) { #> 
     95                    <p class="themes-filter-container"> 
     96                        <label for="themes-filter"> 
     97                            <span class="screen-reader-text"><?php _e( 'Search installed themes&hellip;' ); ?></span> 
     98                            <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search installed themes&hellip;' ); ?>" /> 
     99                        </label> 
     100                    </p> 
     101                <# } #> 
     102            <?php endif; ?> 
     103            <# if ( 'favorites' === data.action ) { #> 
     104                <div class="favorites-form filter-details"> 
     105                    <p class="install-help"><?php _e( 'If you have marked themes as favorites on WordPress.org, you can browse them here.' ); ?></p> 
     106                    <p> 
     107                        <label for="wporg-username-input"><?php _e( 'Your WordPress.org username:' ); ?></label> 
     108                        <input type="search" id="wporg-username-input" value=""> 
     109                        <button type="button" class="button button-secondary favorites-form-submit"><?php _e( 'Get Favorites' ); ?></button> 
     110                    </p> 
     111                </div> 
     112            <# } else if ( 'feature_filter' === data.action ) { #> 
     113                <div class="filter-drawer filter-details"> 
    60114                    <?php 
    61                     if ( $this->manager->is_theme_active() ) { 
    62                         echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title; 
    63                     } else { 
    64                         echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title; 
     115                    $feature_list = get_theme_feature_list(); 
     116                    foreach ( $feature_list as $feature_name => $features ) { 
     117                        echo '<fieldset class="filter-group">'; 
     118                        $feature_name = esc_html( $feature_name ); 
     119                        echo '<legend><button type="button" class="button-link" aria-expanded="false">' . $feature_name . '</button></legend>'; 
     120                        echo '<div class="filter-group-feature">'; 
     121                        foreach ( $features as $feature => $feature_name ) { 
     122                            $feature = esc_attr( $feature ); 
     123                            echo '<input type="checkbox" id="filter-id-' . $feature . '" value="' . $feature . '" /> '; 
     124                            echo '<label for="filter-id-' . $feature . '">' . $feature_name . '</label><br>'; 
     125                        } 
     126                        echo '</div>'; 
     127                        echo '</fieldset>'; 
    65128                    } 
    66129                    ?> 
    67                     <button type="button" class="button customize-theme"><?php _e( 'Customize' ); ?></button> 
    68                 </h3> 
    69  
     130                </div> 
     131            <# } #> 
     132            <div class="customize-themes-section themes-section-{{ data.id }} control-section-content themes-php"> 
    70133                <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div> 
    71  
    72                 <div id="customize-container"></div> 
    73                 <?php if ( count( $this->controls ) > 4 ) : ?> 
    74                     <p><label for="themes-filter"> 
    75                         <span class="screen-reader-text"><?php _e( 'Search installed themes&hellip;' ); ?></span> 
    76                         <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search installed themes&hellip;' ); ?>" /> 
    77                     </label></p> 
    78                 <?php endif; ?> 
    79134                <div class="theme-browser rendered"> 
    80                     <ul class="themes accordion-section-content"> 
     135                    <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> 
     136                    <ul class="themes"> 
    81137                    </ul> 
     138                    <p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p> 
     139                    <p class="spinner"></p> 
    82140                </div> 
    83141            </div> 
  • trunk/tests/phpunit/tests/customize/manager.php

    r38810 r38813  
    14871487        $this->assertNotEmpty( $data ); 
    14881488 
    1489         $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts' ), array_keys( $data ) ); 
     1489        $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts', 'l10n' ), array_keys( $data ) ); 
    14901490        $this->assertEquals( $autofocus, $data['autofocus'] ); 
    14911491        $this->assertArrayHasKey( 'save', $data['nonce'] ); 
Note: See TracChangeset for help on using the changeset viewer.