Make WordPress Core

Ticket #37661: 37661.diff

File 37661.diff, 69.6 KB (added by celloexpressions, 10 years ago)

First pass, see completed and pending to-do lists above.

Line 
1Index: src/wp-admin/css/customize-controls.css
2===================================================================
3--- src/wp-admin/css/customize-controls.css (revision 38260)
4+++ src/wp-admin/css/customize-controls.css (working copy)
5@@ -975,29 +975,25 @@
6 animation: customize-reload .75s;
7 }
8
9-.control-section-themes .accordion-section-title {
10+.control-panel-themes .accordion-section-title {
11 cursor: default;
12 }
13
14-#customize-theme-controls .control-section-themes .accordion-section-title:hover,
15-#customize-theme-controls .control-section-themes .accordion-section-title:focus {
16+#customize-theme-controls .control-panel-themes > .accordion-section-title:hover,
17+#customize-theme-controls .control-panel-themes > .accordion-section-title:focus {
18 color: #555;
19 background-color: #fff;
20 }
21
22-.control-section-themes .accordion-section-title {
23+#customize-theme-controls .control-panel-themes > .accordion-section-title {
24 margin: 15px 0;
25-}
26-
27-.customize-themes-panel .accordion-section-title {
28- margin: 15px -8px;
29-}
30-
31-.control-section-themes .accordion-section-title {
32 padding-right: 100px; /* Space for the button */
33+ border-top: 1px solid #ddd;
34+ border-bottom: 1px solid #ddd;
35+ border-right: 0;
36 }
37
38-.control-section-themes .accordion-section-title span.customize-action,
39+.control-panel-themes .accordion-section-title span.customize-action,
40 #customize-controls .customize-section-title span.customize-action {
41 font-size: 13px;
42 display: block;
43@@ -1004,8 +1000,7 @@
44 font-weight: 400;
45 }
46
47-.control-section-themes .accordion-section-title .change-theme,
48-.control-section-themes .accordion-section-title .customize-theme {
49+.control-panel-themes .accordion-section-title .change-theme {
50 position: absolute;
51 right: 10px;
52 top: 50%;
53@@ -1013,45 +1008,191 @@
54 font-weight: 400;
55 }
56
57-.control-section-themes .accordion-section-title:before {
58+#customize-theme-controls .control-panel-themes > .accordion-section-title:after {
59 display: none;
60 }
61
62-.customize-themes-panel {
63+.control-panel-themes .control-panel-content {
64 display: none;
65- padding: 0 8px;
66- background: #f1f1f1;
67- -webkit-box-sizing: border-box;
68- -moz-box-sizing: border-box;
69- box-sizing: border-box;
70+ position: fixed;
71+ width: 100%;
72+ height: 100%;
73+ top: 0;
74+ left: -100%;
75+ background: #eee;
76+ z-index: 10;
77+ transition: .2s left ease-in-out;
78+ margin: 0;
79+ padding: 0;
80+ overflow-y: auto;
81 }
82
83-.customize-themes-panel .accordion-section-title:first-child {
84- margin-top: 0;
85+.in-themes-panel .control-panel-themes .control-panel-content {
86+ left: 0;
87 }
88
89-#customize-controls .customize-themes-panel .accordion-section-title:nth-child(2) {
90- font-size: 14px;
91- font-weight: 600;
92+#customize-controls,
93+#customize-footer-actions,
94+#customize-footer-actions .collapse-sidebar {
95+ left: 0;
96+ transition: .2s left ease-in-out;
97 }
98
99+.in-themes-panel #customize-controls,
100+.in-themes-panel #customize-preview,
101+.in-themes-panel #customize-footer-actions,
102+.in-themes-panel #customize-footer-actions .collapse-sidebar {
103+ left: 100%;
104+}
105+
106 .customize-themes-panel > h2 {
107 padding: 15px 8px 0 8px;
108 }
109
110-.control-section.open .customize-themes-panel {
111- display: block;
112+.control-panel-themes .panel-meta .current-theme {
113+ position: absolute;
114+ right: 0;
115+ top: 0;
116+ background: #fff;
117+ border-left: 1px solid #ddd;
118+ padding: 15px 125px 15px 25px;
119 }
120
121-#customize-theme-controls .customize-themes-panel .accordion-section-content {
122+.control-panel-themes .panel-meta .customize-theme {
123+ position: absolute;
124+ bottom: 15px;
125+ right: 25px;
126+}
127+
128+.control-panel-themes .panel-meta h2 {
129+ font-size: 32px;
130+ font-weight: 200;
131+ margin: 0 25px;
132+ padding: 15px 0;
133+ line-height: 50px;
134+ float: left;
135+}
136+
137+.control-panel-themes .panel-meta {
138+ min-height: 80px;
139+ overflow: hidden;
140+}
141+
142+.control-panel-themes .panel-meta .page-title-action {
143+ margin-top: 28px;
144+}
145+
146+/* Mobile header */
147+@media screen and (max-width:700px) {
148+ .control-panel-themes .panel-meta .current-theme {
149+ width: calc(100% - 150px);
150+ position: relative;
151+ border-bottom: 1px solid #ddd;
152+ }
153+}
154+
155+.control-panel-themes .customize-themes-notifications .notice {
156+ margin: 0 25px 15px 25px;
157+}
158+
159+#customize-theme-section-navigation {
160+ margin: 0 25px 15px 25px;
161+ width: calc(100% - 50px);
162+}
163+
164+.control-panel-themes .wp-filter .customize-help-toggle {
165+ float: right;
166+ padding: 10px;
167+ height: 40px;
168+ width: 40px;
169+ margin: 5px;
170 background: transparent;
171- display: block;
172+ border: none;
173+ box-shadow: none;
174+ cursor: pointer;
175+ color: #555;
176 }
177
178-.customize-control.customize-control-theme {
179- margin-bottom: 8px;
180+.control-panel-themes .wp-filter .customize-help-toggle:hover {
181+ color: #0073aa;
182 }
183
184+.control-panel-themes .wp-filter .customize-help-toggle:focus,
185+.control-panel-themes .wp-filter .customize-help-toggle.toggled {
186+ color: #0073aa;
187+ border-radius: 100%;
188+ box-shadow: 0 0 0 1px #5b9dd9,
189+ 0 0 2px 1px rgba(30, 140, 190, .8);
190+ outline: 0;
191+}
192+
193+.control-panel-themes .customize-panel-description {
194+ position: absolute;
195+ right: -1px;
196+ z-index: 5;
197+ background: #fff;
198+ font-size: 14px;
199+ width: 300px;
200+ border: 1px solid #ddd;
201+ border-top: none;
202+ padding: 5px 25px 15px 10px;
203+ text-align: right;
204+ display: none;
205+}
206+
207+.control-panel-themes .theme-section {
208+ width: calc(100% - 50px);
209+ padding: 0 25px 25px 25px;
210+ margin: 0;
211+ display: none !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
212+ overflow: hidden;
213+}
214+
215+.control-panel-themes .theme-section.current-section {
216+ display: list-item !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
217+}
218+
219+#customize-theme-section-navigation .customize-themes-section-title {
220+ display: inline-block;
221+ margin: 0 10px;
222+ padding: 15px 0;
223+ background: #fff;
224+ border: 0;
225+ border-bottom: 4px solid #fff;
226+ color: #555;
227+ cursor: pointer;
228+}
229+
230+.filter-links .themes-section-feature_filter_themes:before {
231+ content: "\f111";
232+ font: 16px/1 dashicons;
233+ position: relative;
234+ top: 2px;
235+ margin-right: 4px;
236+}
237+
238+#customize-theme-section-navigation .customize-themes-section-title.themes-section-search_themes {
239+ border-bottom: none;
240+ padding: 10px 0;
241+}
242+
243+#customize-theme-section-navigation .customize-themes-section-title.selected {
244+ border-bottom: 4px solid #555;
245+}
246+
247+#customize-theme-section-navigation .customize-themes-section-title:hover,
248+#customize-theme-section-navigation .customize-themes-section-title:focus {
249+ color: #0073aa;
250+ border-bottom: 4px solid #0073aa;
251+ box-shadow: none;
252+ outline: none;
253+}
254+
255+#customize-theme-section-navigation .customize-themes-section-title.themes-section-search_themes.selected,
256+#customize-theme-section-navigation .customize-themes-section-title.themes-section-search_themes:hover {
257+ border-bottom: none;
258+}
259+
260 #customize-theme-controls .themes.accordion-section-content {
261 position: relative;
262 left: 0;
263@@ -1059,15 +1200,112 @@
264 width: 100%;
265 }
266
267-.wp-customizer .theme-browser .themes {
268- padding-bottom: 8px;
269+.theme-section.loading .spinner {
270+ display: block;
271+ visibility: visible;
272+ position: relative;
273+ clear: both;
274+ width: 20px;
275+ height: 20px;
276+ left: calc(50% - 10px);
277+ float: none;
278+ margin-top: 50px;
279 }
280
281-.wp-customizer .theme-browser .theme {
282+.customize-themes-section .filter-drawer {
283+ border-top: none;
284+ display: block;
285+ background: transparent;
286+ padding-top: 5px;
287+}
288+
289+.customize-themes-section .clear-filters {
290+ margin-left: 8px;
291+ display: none;
292+}
293+
294+#accordion-section-feature_filter_themes .theme-browser {
295+ display: none; /* Shown with JS once filters are applied */
296+}
297+
298+.customize-themes-section .no-themes {
299+ display: none;
300+}
301+
302+#accordion-section-installed_themes .theme .notice-success {
303+ display: none;
304+}
305+
306+.control-panel-themes .theme-browser .theme .theme-actions .button-primary {
307+ margin: 0 0 0 8px;
308+}
309+
310+.customize-control-theme .theme {
311+ width: 100%;
312 margin: 0;
313- width: 100%;
314 }
315
316+.customize-control.customize-control-theme { /* override most properties on .customize-control */
317+ box-sizing: border-box;
318+ width: 18.4%;
319+ margin: 0 2% 2% 0;
320+ padding: 0;
321+ clear: none;
322+}
323+
324+/* 5 columns above 2100px */
325+@media screen and (min-width: 2101px) {
326+ .customize-control.customize-control-theme:nth-child(5n) {
327+ margin-right: 0;
328+ }
329+}
330+
331+/* 4 columns up to 2100px */
332+@media screen and (min-width: 1601px) and (max-width: 2100px) {
333+ .customize-control.customize-control-theme {
334+ width: 23.5%;
335+ }
336+
337+ .customize-control.customize-control-theme:nth-child(4n) {
338+ margin-right: 0;
339+ }
340+}
341+
342+/* 3 columns up to 1600px */
343+@media screen and (min-width: 1101px) and (max-width: 1600px) {
344+ .customize-control.customize-control-theme {
345+ width: 32%;
346+ }
347+
348+ .customize-control.customize-control-theme:nth-child(3n) {
349+ margin-right: 0;
350+ }
351+}
352+
353+/* 2 columns up to 1100px */
354+@media screen and (min-width: 501px) and (max-width: 1100px) {
355+ .customize-control.customize-control-theme {
356+ width: 49%;
357+ }
358+
359+ .customize-control.customize-control-theme:nth-child(even) {
360+ margin-right: 0;
361+ }
362+}
363+
364+/* 1 column up to 500 px */
365+@media screen and (max-width: 500px) {
366+ .customize-control.customize-control-theme {
367+ width: 100%;
368+ margin: 0 0 3% 0;
369+ }
370+}
371+
372+
373+.wp-customizer .theme-browser .themes {
374+ padding-bottom: 8px;
375+}
376+
377 .wp-customizer .theme-browser .theme .theme-actions {
378 -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
379 opacity: 1;
380@@ -1088,27 +1326,15 @@
381 width: 100%;
382 }
383
384-#accordion-section-themes .accordion-section-title:after {
385+#accordion-panel-themes .accordion-section-title:after {
386 display: none;
387 }
388
389-#customize-theme-controls .control-section-themes.current-panel > h3.accordion-section-title {
390+#customize-theme-controls .control-panel-themes.current-panel > h3.accordion-section-title {
391 left: 0;
392 }
393
394-.customize-themes-panel.control-panel-content {
395- position: absolute;
396- left: -100%;
397- top: 0;
398- width: 100%;
399- border-top: 1px solid #ddd;
400-}
401
402-.in-themes-panel #customize-info,
403-.in-themes-panel #customize-theme-controls > ul > .accordion-section {
404- left: 100%;
405-}
406-
407 /* Details View */
408 .wp-customizer .theme-overlay {
409 display: none;
410@@ -1129,6 +1355,15 @@
411 z-index: 110;
412 }
413
414+.wp-customizer .theme-overlay .star-rating {
415+ float: left;
416+ margin-right: 8px;
417+}
418+
419+.wp-customizer .theme-rating .num-ratings {
420+ line-height: 20px;
421+}
422+
423 .wp-customizer .theme-overlay .theme-wrap {
424 left: 90px;
425 right: 90px;
426@@ -1139,9 +1374,14 @@
427 }
428
429 .wp-customizer .theme-overlay .theme-actions {
430- text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */
431+ text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */
432+ padding: 10px 15px;
433 }
434
435+.wp-customizer .theme-overlay .theme-actions .theme-install.preview {
436+ margin-left: 8px;
437+}
438+
439 .modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content {
440 overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */
441 }
442Index: src/wp-admin/includes/theme.php
443===================================================================
444--- src/wp-admin/includes/theme.php (revision 38260)
445+++ src/wp-admin/includes/theme.php (working copy)
446@@ -606,7 +606,9 @@
447 * @since 4.2.0
448 */
449 function customize_themes_print_templates() {
450- $preview_url = esc_url( add_query_arg( 'theme', '__THEME__' ) ); // Token because esc_url() strips curly braces.
451+ $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
452+ $active_url = esc_url( remove_query_arg( 'theme', $current_url ) );
453+ $preview_url = esc_url( add_query_arg( 'theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
454 $preview_url = str_replace( '__THEME__', '{{ data.id }}', $preview_url );
455 ?>
456 <script type="text/html" id="tmpl-customize-themes-details-view">
457@@ -619,7 +621,7 @@
458 </div>
459 <div class="theme-about wp-clearfix">
460 <div class="theme-screenshots">
461- <# if ( data.screenshot[0] ) { #>
462+ <# if ( data.screenshot && data.screenshot[0] ) { #>
463 <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div>
464 <# } else { #>
465 <div class="screenshot blank"></div>
466@@ -632,29 +634,46 @@
467 <# } #>
468 <h2 class="theme-name">{{{ data.name }}}<span class="theme-version"><?php printf( __( 'Version: %s' ), '{{ data.version }}' ); ?></span></h2>
469 <h3 class="theme-author"><?php printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' ); ?></h3>
470+
471+ <# if ( data.stars && 0 != data.num_ratings ) { #>
472+ <div class="theme-rating">
473+ {{{ data.stars }}}
474+ <span class="num-ratings"><?php echo sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ); ?></span>
475+ </div>
476+ <# } #>
477+
478+ <# if ( false == false && data.hasUpdate ) { // @todo patch updates.js to work in the customizer #>
479+ <div class="notice notice-warning notice-alt notice-large">
480+ <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
481+ {{{ data.update }}}
482+ </div>
483+ <# } #>
484+
485 <p class="theme-description">{{{ data.description }}}</p>
486
487 <# if ( data.parent ) { #>
488 <p class="parent-theme"><?php printf( __( 'This is a child theme of %s.' ), '<strong>{{{ data.parent }}}</strong>' ); ?></p>
489 <# } #>
490-
491 <# if ( data.tags ) { #>
492- <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{ data.tags }}</p>
493+ <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
494 <# } #>
495 </div>
496 </div>
497
498- <# if ( ! data.active ) { #>
499- <div class="theme-actions">
500- <div class="inactive-theme">
501- <?php
502- /* translators: %s: Theme name */
503- $aria_label = sprintf( __( 'Preview %s' ), '{{ data.name }}' );
504- ?>
505- <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>
506- </div>
507- </div>
508- <# } #>
509+ <div class="theme-actions">
510+ <# if ( data.active ) { #>
511+ <?php
512+ /* translators: %s: Theme name */
513+ $aria_label = sprintf( __( 'Preview %s' ), '{{ data.name }}' );
514+ ?>
515+ <button type="button" class="button button-primary customize-theme" aria-label="<?php echo esc_attr( $aria_label ); ?>"><?php _e( 'Customize' ); ?></a>
516+ <# } else if ( 'installed' === data.type ) { #>
517+ <button type="button" class="button button-primary preview-theme" data-preview-url="<?php echo esc_attr( $preview_url ); ?>" aria-label="<?php echo esc_attr( $aria_label ); ?>"><?php _e( 'Live Preview' ); ?></span>
518+ <# } else { #>
519+ <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
520+ <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}" data-previewurl="<?php echo esc_attr( $preview_url ); ?>"><?php _e( 'Install & Preview' ); ?></button>
521+ <# } #>
522+ </div>
523 </div>
524 </script>
525 <?php
526Index: src/wp-admin/js/customize-controls.js
527===================================================================
528--- src/wp-admin/js/customize-controls.js (revision 38260)
529+++ src/wp-admin/js/customize-controls.js (working copy)
530@@ -774,8 +775,8 @@
531 /**
532 * wp.customize.ThemesSection
533 *
534- * Custom section for themes that functions similarly to a backwards panel,
535- * and also handles the theme-details view rendering and navigation.
536+ * Custom section for themes that loads themes by category, and also
537+ * handles the theme-details view rendering and navigation.
538 *
539 * @constructor
540 * @augments wp.customize.Section
541@@ -787,18 +788,14 @@
542 template: '',
543 screenshotQueue: null,
544 $window: $( window ),
545+ loaded: 0,
546+ loading: false,
547+ fullyLoaded: false,
548+ term: '',
549
550 /**
551 * @since 4.2.0
552 */
553- initialize: function () {
554- this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
555- return api.Section.prototype.initialize.apply( this, arguments );
556- },
557-
558- /**
559- * @since 4.2.0
560- */
561 ready: function () {
562 var section = this;
563 section.overlay = section.container.find( '.theme-overlay' );
564@@ -826,7 +823,7 @@
565 }
566 });
567
568- _.bindAll( this, 'renderScreenshots' );
569+ _.bindAll( this, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked', 'clearFilters' );
570 },
571
572 /**
573@@ -833,8 +830,8 @@
574 * Override Section.isContextuallyActive method.
575 *
576 * Ignore the active states' of the contained theme controls, and just
577- * use the section's own active state instead. This ensures empty search
578- * results for themes to cause the section to become inactive.
579+ * use the section's own active state instead. This prevents empty search
580+ * results for theme sections from causing the section to become inactive.
581 *
582 * @since 4.2.0
583 *
584@@ -850,20 +847,22 @@
585 attachEvents: function () {
586 var section = this;
587
588- // Expand/Collapse section/panel.
589- section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
590- if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
591- return;
592- }
593- event.preventDefault(); // Keep this AFTER the key filter above
594-
595- if ( section.expanded() ) {
596- section.collapse();
597- } else {
598+ // Expand section/panel. Only collapse when opening another section.
599+ $( '#customize-theme-section-navigation' ).on( 'click', '.themes-section-' + section.id, function( event ) {
600+ if ( ! section.expanded() ) {
601 section.expand();
602 }
603 });
604
605+ // Preview installed themes.
606+ section.container.on( 'click', '.theme-actions .preview-theme', function() {
607+ var previewUrl = $( this ).data( 'previewUrl' );
608+
609+ $( '.wp-full-overlay' ).addClass( 'customize-loading' );
610+
611+ window.parent.location = previewUrl;
612+ });
613+
614 // Theme navigation in details view.
615 section.container.on( 'click keydown', '.left', function( event ) {
616 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
617@@ -909,18 +908,46 @@
618
619 // Update theme count.
620 count = section.container.find( 'li.customize-control:visible' ).length;
621- section.container.find( '.theme-count' ).text( count );
622+ $( '.control-panel-themes' ).find( '.theme-count' ).text( count );
623 });
624
625- // Pre-load the first 3 theme screenshots.
626- api.bind( 'ready', function () {
627- _.each( section.controls().slice( 0, 3 ), function ( control ) {
628- var img, src = control.params.theme.screenshot[0];
629- if ( src ) {
630- img = new Image();
631- img.src = src;
632+ // Event listeners for queries with user-entered terms.
633+ if ( 'search' === section.params.action ) {
634+ var debounced = _.debounce( section.checkTerm, 800 ); // Wait until there is no input for 800 miliseconds to initiate a search.
635+ $( '#customize-theme-section-navigation' ).on( 'keydown', '#wp-filter-search-input', function() {
636+ debounced( section );
637+ });
638+ } else if ( 'favorites' === section.params.action ) {
639+ section.container.on( 'click', '.favorites-form-submit', function() {
640+ section.checkTerm( section );
641+ });
642+ section.container.on( 'keydown', '#wporg-username-input', function( e ) {
643+ if ( api.utils.isKeydownButNotEnterEvent( e ) ) {
644+ return;
645 }
646+ section.checkTerm( section );
647 });
648+ } else if ( 'feature_filter' === section.params.action ) {
649+ section.container.on( 'click', '.filter-group input', function() {
650+ section.filtersChecked();
651+ });
652+ section.container.on( 'click', '.clear-filters', function() {
653+ section.clearFilters();
654+ });
655+ section.container.on( 'click', '.apply-filters', function() {
656+ section.checkTerm( section );
657+ });
658+ section.container.on( 'click', '.filtered-by .tags, .filtered-by button', function() {
659+ section.container.find( '.filter-group' ).show();
660+ section.container.find( '.buttons' ).show();
661+ section.container.find( '.filtered-by' ).hide();
662+ section.container.find( '.theme-browser' ).hide();
663+ });
664+ }
665+
666+ // Move section-expansion buttons to consolidated menu bar in themes panel.
667+ api.bind( 'ready', function () {
668+ section.container.find( '.customize-themes-section-title' ).appendTo( $( '#customize-theme-section-navigation .filter-links' ) );
669 });
670 },
671
672@@ -945,70 +972,292 @@
673 }
674
675 // Note: there is a second argument 'args' passed
676- var position, scroll,
677- panel = this,
678- section = panel.container.closest( '.accordion-section' ),
679- overlay = section.closest( '.wp-full-overlay' ),
680- container = section.closest( '.wp-full-overlay-sidebar-content' ),
681- siblings = container.find( '.open' ),
682- customizeBtn = section.find( '.customize-theme' ),
683- changeBtn = section.find( '.change-theme' ),
684- content = section.find( '.control-panel-content' );
685+ var section = this,
686+ overlay = section.container.closest( '.wp-full-overlay-sidebar-content' ),
687+ container = section.container.closest( '.control-panel-content' );
688
689 if ( expanded ) {
690
691+ // Load controls if none are loaded yet.
692+ if ( 0 === section.loaded ) {
693+ section.loadControls();
694+ }
695+
696 // Collapse any sibling sections/panels
697 api.section.each( function ( otherSection ) {
698- if ( otherSection !== panel ) {
699+ if ( otherSection !== section ) {
700 otherSection.collapse( { duration: args.duration } );
701 }
702 });
703- api.panel.each( function ( otherPanel ) {
704- otherPanel.collapse( { duration: 0 } );
705- });
706
707- content.show( 0, function() {
708- position = content.offset().top;
709- scroll = container.scrollTop();
710- content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
711- section.addClass( 'current-panel' );
712- overlay.addClass( 'in-themes-panel' );
713- container.scrollTop( 0 );
714- _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
715- panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
716- if ( args.completeCallback ) {
717- args.completeCallback();
718- }
719- } );
720- customizeBtn.focus();
721+ section.container.addClass( 'current-section' );
722+ $( '#customize-theme-section-navigation .themes-section-' + section.id ).addClass( 'selected' );
723+ container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
724+ container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
725+ if ( args.completeCallback ) {
726+ args.completeCallback();
727+ }
728+ section.updateCount(); // Show this section's count.
729 } else {
730- siblings.removeClass( 'open' );
731- section.removeClass( 'current-panel' );
732- overlay.removeClass( 'in-themes-panel' );
733- panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
734- content.delay( 180 ).hide( 0, function() {
735- content.css( 'margin-top', 'inherit' ); // Reset
736- if ( args.completeCallback ) {
737- args.completeCallback();
738- }
739- } );
740- customizeBtn.attr( 'tabindex', '0' );
741- changeBtn.focus();
742- container.scrollTop( 0 );
743+ section.container.removeClass( 'current-section' );
744+ $( '#customize-theme-section-navigation .themes-section-' + section.id ).removeClass( 'selected' );
745+ container.off( 'scroll' );
746+ if ( args.completeCallback ) {
747+ args.completeCallback();
748+ }
749 }
750 },
751
752 /**
753- * Recalculate the top margin.
754+ * Don't recalculate the top margin.
755 *
756- * @since 4.4.0
757+ * @since 4.7.0
758 * @private
759 */
760- _recalculateTopMargin: function() {
761- api.Panel.prototype._recalculateTopMargin.call( this );
762+ _recalculateTopMargin: function() {},
763+
764+ /**
765+ * Load theme data via ajax and add themes to the section as controls.
766+ *
767+ * @since 4.7.0
768+ */
769+ loadControls: function() {
770+ var section = this, params, page, request, search;
771+
772+ if ( section.loading ) {
773+ return; // We're already loading a batch of themes.
774+ }
775+
776+ // Parameters for every API query. Additional params are set in PHP.
777+ page = Math.ceil( section.loaded / 100 ) + 1;
778+ params = {
779+ 'customize-themes-nonce': api.settings.nonce['customize-themes'],
780+ 'wp_customize': 'on',
781+ 'theme_action': section.params.action,
782+ 'customized_theme': api.settings.theme.stylesheet,
783+ 'page': page
784+ }
785+
786+ // Add fields for special request actions.
787+ if ( 'search' === section.params.action ) {
788+ if ( '' === section.term ) {
789+ return;
790+ } else {
791+ params.search = section.term;
792+ }
793+ } else if ( 'favorites' === section.params.action ) {
794+ if ( '' === section.term ) {
795+ return;
796+ } else {
797+ params.user = section.term;
798+ }
799+ } else if ( 'feature_filter' === section.params.action ) {
800+ if ( '' === section.term ) {
801+ return;
802+ } else {
803+ params.tags = section.term;
804+ }
805+ }
806+
807+ // Load themes.
808+ section.container.addClass( 'loading' );
809+ section.loading = true;
810+ section.container.find( '.no-themes' ).hide();
811+ request = wp.ajax.post( 'customize-load-themes', params );
812+ request.done(function( data ) {
813+ var themes = data.themes,
814+ themeControl, newThemeControls;
815+ if ( 0 !== themes.length ) {
816+ newThemeControls = new Array();
817+ // Add controls for each theme.
818+ _.each( themes, function ( theme ) {
819+ customizeId = section.params.action + '_theme_' + theme.id;
820+ themeControl = new api.controlConstructor.theme( customizeId, {
821+ params: {
822+ type: 'theme',
823+ content: '<li id="customize-control-theme-' + section.params.action + '_' + theme.id + '" class="customize-control customize-control-theme"></li>',
824+ section: section.params.id,
825+ active: true,
826+ theme: theme,
827+ priority: section.loaded + 1
828+ },
829+ previewer: api.previewer
830+ } );
831+
832+ api.control.add( customizeId, themeControl );
833+ newThemeControls.push( themeControl );
834+ section.loaded = section.loaded + 1;
835+ return;
836+ });
837+
838+ if ( 1 === page ) {
839+ // Pre-load the first 3 theme screenshots.
840+ _.each( section.controls().slice( 0, 3 ), function ( control ) {
841+ var img, src = control.params.theme.screenshot[0];
842+ if ( src ) {
843+ img = new Image();
844+ img.src = src;
845+ }
846+ });
847+ } else {
848+ Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
849+ }
850+ _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
851+
852+ if ( 'installed' === section.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
853+ section.fullyLoaded = true;
854+ }
855+ } else {
856+ if ( 0 === section.loaded ) {
857+ section.container.find( '.no-themes' ).show();
858+ } else {
859+ section.fullyLoaded = true;
860+ }
861+ }
862+ if ( 'installed' === section.params.action ) {
863+ section.updateCount();
864+ } else {
865+ section.updateCount( data.info.results );
866+ }
867+ section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
868+
869+ // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
870+ section.container.removeClass( 'loading' );
871+ section.loading = false;
872+ });
873+ request.fail(function( data ) {
874+ if ( 'undefined' === typeof data ) {
875+ section.container.find( '.unexpected-error' ).show();
876+ } else if ( typeof console !== 'undefined' && console.error ) {
877+ console.error( data );
878+ }
879+
880+ // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
881+ section.container.removeClass( 'loading' );
882+ section.loading = false;
883+ });
884 },
885
886 /**
887+ * Determines whether more themes should be loaded, and loads them.
888+ *
889+ * @since 4.7.0
890+ */
891+ loadMore: function() {
892+ var section = this, container, bottom, threshold, page;
893+ if ( ! section.fullyLoaded && ! section.loading ) {
894+ container = section.container.closest( '.control-panel-content' );
895+
896+ bottom = container.scrollTop() + container.height();
897+ 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.
898+
899+ if ( bottom > threshold ) {
900+ section.loadControls();
901+ }
902+ }
903+ },
904+
905+ /**
906+ * Event handler for search, feature filter, and favorites input that determines if the term has changed and loads new controls as needed.
907+ *
908+ * @since 4.7.0
909+ *
910+ * @param api.ThemesSection section The current theme section, passed through the debouncer.
911+ */
912+ checkTerm: function( section ) {
913+ var newTerm, filteringBy;
914+
915+ // Find term.
916+ if ( 'search' === section.params.action ) {
917+ newTerm = $( '#wp-filter-search-input' ).val();
918+ } else if ( 'favorites' === section.params.action ) {
919+ newTerm = $( '#wporg-username-input' ).val();
920+ } else if ( 'feature_filter' === section.params.action ) {
921+ newTerm = section.term; // Set separately by filtersChecked(), as they're changed.
922+ if ( '' === newTerm ) {
923+ return;
924+ }
925+
926+ section.container.find( '.filter-group' ).hide();
927+ section.container.find( '.buttons' ).hide();
928+ section.container.find( '.filtered-by' ).show();
929+ section.container.find( '.theme-browser' ).show();
930+ filteringBy = section.container.find( '.filtered-by .tags' );
931+ filteringBy.empty();
932+
933+ _.each( newTerm, function( tag ) {
934+ name = $( 'label[for="filter-id-' + tag + '"]' ).text();
935+ filteringBy.append( '<span class="tag">' + name + '</span>' );
936+ });
937+ } else {
938+ return;
939+ }
940+
941+ if ( section.term === newTerm && 'feature_filter' !== section.params.action ) {
942+ return;
943+ }
944+ // Clear the controls in the section.
945+ _.each( section.controls(), function( control ) {
946+ control.container.remove();
947+ api.control.remove( control.id );
948+ });
949+ section.loaded = 0;
950+ section.fullyLoaded = false;
951+ section.screenshotQueue = null;
952+
953+ if ( '' !== newTerm ) { // Empty term should not show any results.
954+ // Run a new query, with loadControls handling paging, etc.
955+ section.term = newTerm;
956+ section.loadControls();
957+ }
958+ },
959+
960+ /**
961+ * Check for filters checked in the feature filter list.
962+ *
963+ * @since 4.7.0
964+ */
965+ filtersChecked: function() {
966+ var section = this,
967+ items = section.container.find( '.filter-group' ).find( ':checkbox' ),
968+ tags = [];
969+
970+ if ( 'feature_filter' !== section.params.action ) {
971+ return false;
972+ }
973+
974+ _.each( items.filter( ':checked' ), function( item ) {
975+ tags.push( $( item ).prop( 'value' ) );
976+ });
977+
978+ // When no filters are checked, restore initial state and return
979+ if ( tags.length === 0 ) {
980+ section.container.find( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
981+ section.container.find( '.filter-drawer .clear-filters' ).hide();
982+ section.term = '';
983+ } else {
984+ section.container.find( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
985+ section.container.find( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' ); // $.show() manually.
986+ section.term = tags;
987+ }
988+ },
989+
990+ /**
991+ * Clear filters from the feature filter list.
992+ *
993+ * @since 4.7.0
994+ */
995+ clearFilters: function() {
996+ var section = this,
997+ items = section.container.find( '.filter-group' ).find( ':checkbox' );
998+
999+ _.each( items.filter( ':checked' ), function( item ) {
1000+ $( item ).prop( 'checked', false );
1001+ });
1002+ section.filtersChecked();
1003+ },
1004+
1005+ /**
1006 * Render control's screenshot if the control comes into view.
1007 *
1008 * @since 4.2.0
1009@@ -1016,12 +1265,15 @@
1010 renderScreenshots: function( ) {
1011 var section = this;
1012
1013- // Fill queue initially.
1014- if ( section.screenshotQueue === null ) {
1015- section.screenshotQueue = section.controls();
1016+ // Fill queue initially, or check for more if empty.
1017+ if ( section.screenshotQueue === null || 0 === section.screenshotQueue.length ) {
1018+ // Add controls that haven't had their screenshots rendered.
1019+ section.screenshotQueue = _.filter( section.controls(), function( control ) {
1020+ return ! control.screenshotRendered;
1021+ });
1022 }
1023
1024- // Are all screenshots rendered?
1025+ // Are all screenshots rendered (for now)?
1026 if ( ! section.screenshotQueue.length ) {
1027 return;
1028 }
1029@@ -1057,6 +1309,18 @@
1030 },
1031
1032 /**
1033+ * Update the number of themes in the section.
1034+ *
1035+ * @since 4.7.0
1036+ */
1037+ updateCount: function ( count ) {
1038+ if ( ! count ) {
1039+ count = this.loaded;
1040+ }
1041+ this.container.closest( '.control-panel-content' ).find( '.theme-count' ).text( count );
1042+ },
1043+
1044+ /**
1045 * Advance the modal to the next theme.
1046 *
1047 * @since 4.2.0
1048@@ -1076,13 +1340,13 @@
1049 * @since 4.2.0
1050 */
1051 getNextTheme: function () {
1052- var control, next;
1053- control = api.control( 'theme_' + this.currentTheme );
1054+ var section = this, control, next;
1055+ control = api.control( section.params.action + '_theme_' + this.currentTheme );
1056 next = control.container.next( 'li.customize-control-theme' );
1057 if ( ! next.length ) {
1058 return false;
1059 }
1060- next = next[0].id.replace( 'customize-control-', '' );
1061+ next = next[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
1062 control = api.control( next );
1063
1064 return control.params.theme;
1065@@ -1108,13 +1372,13 @@
1066 * @since 4.2.0
1067 */
1068 getPreviousTheme: function () {
1069- var control, previous;
1070- control = api.control( 'theme_' + this.currentTheme );
1071+ var section = this, control, previous;
1072+ control = api.control( section.params.action + '_theme_' + this.currentTheme );
1073 previous = control.container.prev( 'li.customize-control-theme' );
1074 if ( ! previous.length ) {
1075 return false;
1076 }
1077- previous = previous[0].id.replace( 'customize-control-', '' );
1078+ previous = previous[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
1079 control = api.control( previous );
1080
1081 return control.params.theme;
1082@@ -1162,7 +1426,7 @@
1083 closeDetails: function () {
1084 $( 'body' ).removeClass( 'modal-open' );
1085 this.overlay.fadeOut( 'fast' );
1086- api.control( 'theme_' + this.currentTheme ).focus();
1087+ api.control( this.params.action + '_theme_' + this.currentTheme ).container.find( '.theme' ).focus();
1088 },
1089
1090 /**
1091@@ -1452,7 +1716,252 @@
1092 }
1093 });
1094
1095+
1096 /**
1097+ * wp.customize.ThemesPanel
1098+ *
1099+ * Custom section for themes that displays without the customize preview.
1100+ *
1101+ * @constructor
1102+ * @augments wp.customize.Panel
1103+ * @augments wp.customize.Container
1104+ */
1105+ api.ThemesPanel = api.Panel.extend({
1106+ installingThemes: [],
1107+
1108+ /**
1109+ * @since 4.7.0
1110+ */
1111+ attachEvents: function () {
1112+ var panel = this;
1113+
1114+ // Expand/Collapse panel.
1115+ panel.container.on( 'click', '.change-theme, .customize-theme', function( event ) {
1116+ if ( panel.expanded() ) {
1117+ panel.collapse();
1118+ } else {
1119+ panel.expand();
1120+ }
1121+ });
1122+
1123+ // Toggle help display.
1124+ panel.container.on( 'click', '.customize-help-toggle', function() {
1125+ var button = $( this ),
1126+ content = button.next( '.customize-panel-description' );
1127+ button.toggleClass( 'toggled' );
1128+ content.toggle();
1129+ if ( button.hasClass( 'toggled' ) ) {
1130+ button.attr( 'aria-expanded', 'true' );
1131+ } else {
1132+ button.attr( 'aria-expanded', 'false' );
1133+ }
1134+ });
1135+
1136+ api.bind( 'saved', function() {
1137+ panel.container.find( '.customize-themes-unsaved-changes' ).hide();
1138+ });
1139+
1140+ // Save & publish customizer changes.
1141+ panel.container.on( 'click', '#customize-themes-save', function() {
1142+ $( '#save' ).click(); // Trigger customizer save.
1143+ panel.container.find( '.customize-themes-unsaved-changes' ).hide();
1144+ api.section( 'installed_themes' ).focus();
1145+ });
1146+
1147+ // Toggle theme upload view.
1148+ panel.container.on( 'click', '.upload-toggle', function() {
1149+ var button = $( this );
1150+ panel.container.find( '.upload-theme' ).toggle();
1151+ if ( 'true' === button.attr( 'aria-expanded' ) ) {
1152+ button.attr( 'aria-expanded', 'false' );
1153+ } else {
1154+ button.attr( 'aria-expanded', 'true' );
1155+ }
1156+ });
1157+
1158+ // Install theme.
1159+ panel.container.on( 'click', '.theme-install', function( event ) {
1160+ panel.installTheme( event );
1161+ });
1162+
1163+ // Update theme.
1164+ panel.container.on( 'click', '.update-theme', function( event ) {
1165+ panel.updateTheme( event );
1166+ });
1167+
1168+ _.bindAll( this, 'installTheme', 'updateTheme' );
1169+ },
1170+
1171+ /**
1172+ * Update UI to reflect expanded state
1173+ *
1174+ * @since 4.7.0
1175+ *
1176+ * @param {Boolean} expanded
1177+ * @param {Object} args
1178+ * @param {Boolean} args.unchanged
1179+ * @param {Callback} args.completeCallback
1180+ */
1181+ onChangeExpanded: function ( expanded, args ) {
1182+
1183+ // Immediately call the complete callback if there were no changes
1184+ if ( args.unchanged ) {
1185+ if ( args.completeCallback ) {
1186+ args.completeCallback();
1187+ }
1188+ return;
1189+ }
1190+
1191+ // Note: there is a second argument 'args' passed
1192+ var panel = this,
1193+ section = panel.container.closest( '.accordion-section' ),
1194+ overlay = section.closest( '.wp-full-overlay' ),
1195+ customizeBtn = section.find( '.customize-theme' ),
1196+ changeBtn = section.find( '.change-theme' ),
1197+ content = section.find( '.control-panel-content' );
1198+
1199+ if ( expanded ) {
1200+ content.show( 0, function() {
1201+ overlay.addClass( 'in-themes-panel' );
1202+ section.addClass( 'current-panel' );
1203+ customizeBtn.attr( 'tabindex', '0' );
1204+ customizeBtn.focus();
1205+ if ( false === api.state( 'saved' ).get() ) {
1206+ panel.container.find( '.customize-themes-unsaved-changes' ).show();
1207+ }
1208+ });
1209+ // Automatically open the installed themes section.
1210+ api.section( 'installed_themes' ).expand();
1211+ } else {
1212+ section.removeClass( 'current-panel' );
1213+ overlay.removeClass( 'in-themes-panel' );
1214+ content.delay( 200 ).hide( 0, function() {
1215+ if ( args.completeCallback ) {
1216+ args.completeCallback();
1217+ }
1218+ } );
1219+ customizeBtn.attr( 'tabindex', '-1' );
1220+ changeBtn.focus();
1221+ }
1222+ },
1223+
1224+ /**
1225+ * Don't recalculate the top margin.
1226+ *
1227+ * @since 4.7.0
1228+ * @private
1229+ */
1230+ _recalculateTopMargin: function() {},
1231+
1232+ /**
1233+ * Install a theme via wp.updates.
1234+ *
1235+ * @since 4.7.0
1236+ */
1237+ installTheme: function( event ) {
1238+ var preview = false, previewUrl,
1239+ slug = $( event.target ).data( 'slug' );
1240+
1241+ if ( -1 !== $.inArray( this.installingThemes, slug ) ) {
1242+ return; // Theme is already being installed.
1243+ }
1244+
1245+ wp.updates.maybeRequestFilesystemCredentials( event );
1246+
1247+ $( document ).on( 'wp-theme-install-success', function( event, response ) {
1248+ var theme = false, customizeId, themeControl;
1249+ if ( preview ) {
1250+ window.parent.location = previewUrl;
1251+ } else {
1252+ api.control.each( function( control ) {
1253+ if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
1254+ theme = control.params.theme; // Used below to add theme control.
1255+ control.rerenderAsInstalled();
1256+ }
1257+ });
1258+
1259+ // Don't add the same theme more than once.
1260+ if ( ! theme || 'undefined' !== typeof api.control( 'installed_theme_' + theme.id ) ) {
1261+ return;
1262+ }
1263+
1264+ // Add theme control to installed section.
1265+ theme.type = 'installed';
1266+ customizeId = 'installed_theme_' + theme.id;
1267+ themeControl = new api.controlConstructor.theme( customizeId, {
1268+ params: {
1269+ type: 'theme',
1270+ content: '<li id="customize-control-theme-installed_' + theme.id + '" class="customize-control customize-control-theme"></li>',
1271+ section: 'installed_themes',
1272+ active: true,
1273+ theme: theme,
1274+ priority: 0 // Add all newly-installed themes to the top.
1275+ },
1276+ previewer: api.previewer
1277+ } );
1278+
1279+ api.control.add( customizeId, themeControl );
1280+ api.control( customizeId ).container.trigger( 'render-screenshot' );
1281+
1282+ // Close the details modal if it's open to the installed theme.
1283+ api.section.each( function( section ) {
1284+ if ( 'themes' !== section.params.type ) {
1285+ if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
1286+ section.closeDetails();
1287+ }
1288+ }
1289+ });
1290+ }
1291+ } );
1292+
1293+ $( document ).on( 'wp-theme-install-failure', function( event, response ) {
1294+ if ( preview ) {
1295+ $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
1296+ }
1297+ });
1298+
1299+ this.installingThemes.push( $( event.target ).data( 'slug' ) ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
1300+ wp.updates.installTheme( {
1301+ slug: $( event.target ).data( 'slug' )
1302+ } );
1303+
1304+ // Also preview the theme as the event is triggered on Install & Preview.
1305+ if ( $( event.target ).hasClass( 'preview' ) ) {
1306+ preview = true;
1307+ $( '.wp-full-overlay' ).addClass( 'customize-loading' );
1308+ previewUrl = $( event.target ).data( 'previewurl' );
1309+ }
1310+ },
1311+
1312+ /**
1313+ * Update a theme via wp.updates.
1314+ *
1315+ * @since 4.7.0
1316+ */
1317+ updateTheme: function( event ) {
1318+ var _this = this;
1319+
1320+ wp.updates.maybeRequestFilesystemCredentials( event );
1321+
1322+ $( document ).on( 'wp-theme-update-success', function( event, response ) {
1323+ // Rerender the control to reflect the update.
1324+ api.control.each( function( control ) {
1325+ if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
1326+ control.params.hasUpdate = false;
1327+ control.rerenderAsInstalled();
1328+ }
1329+ });
1330+ } );
1331+
1332+ wp.updates.updateTheme( {
1333+ slug: $( event.target ).closest( '.notice' ).data( 'slug' )
1334+ } );
1335+ }
1336+ });
1337+
1338+
1339+ /**
1340 * A Customizer Control.
1341 *
1342 * A control provides a UI element that allows a user to modify a Customizer Setting.
1343@@ -2770,35 +3279,11 @@
1344 api.ThemeControl = api.Control.extend({
1345
1346 touchDrag: false,
1347- isRendered: false,
1348+ screenshotRendered: false,
1349
1350 /**
1351- * Defer rendering the theme control until the section is displayed.
1352- *
1353 * @since 4.2.0
1354 */
1355- renderContent: function () {
1356- var control = this,
1357- renderContentArgs = arguments;
1358-
1359- api.section( control.section(), function( section ) {
1360- if ( section.expanded() ) {
1361- api.Control.prototype.renderContent.apply( control, renderContentArgs );
1362- control.isRendered = true;
1363- } else {
1364- section.expanded.bind( function( expanded ) {
1365- if ( expanded && ! control.isRendered ) {
1366- api.Control.prototype.renderContent.apply( control, renderContentArgs );
1367- control.isRendered = true;
1368- }
1369- } );
1370- }
1371- } );
1372- },
1373-
1374- /**
1375- * @since 4.2.0
1376- */
1377 ready: function() {
1378 var control = this;
1379
1380@@ -2822,20 +3307,6 @@
1381 return;
1382 }
1383
1384- var previewUrl = $( this ).data( 'previewUrl' );
1385-
1386- $( '.wp-full-overlay' ).addClass( 'customize-loading' );
1387-
1388- window.parent.location = previewUrl;
1389- });
1390-
1391- control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
1392- if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1393- return;
1394- }
1395-
1396- event.preventDefault(); // Keep this AFTER the key filter above
1397-
1398 api.section( control.section() ).showDetails( control.params.theme );
1399 });
1400
1401@@ -2846,6 +3317,7 @@
1402 if ( source ) {
1403 $screenshot.attr( 'src', source );
1404 }
1405+ control.screenshotRendered = true;
1406 });
1407 },
1408
1409@@ -2866,6 +3338,18 @@
1410 } else {
1411 control.deactivate();
1412 }
1413+ },
1414+
1415+ /**
1416+ * Rerender the theme from its JS template with the installed type.
1417+ *
1418+ * @since 4.2.0
1419+ */
1420+ rerenderAsInstalled: function() {
1421+ var control = this;
1422+ control.params.theme.type = 'installed';
1423+ control.renderContent(); // replaces existing content
1424+ control.container.trigger( 'render-screenshot' );
1425 }
1426 });
1427
1428@@ -3414,7 +3898,9 @@
1429 background: api.BackgroundControl,
1430 theme: api.ThemeControl
1431 };
1432- api.panelConstructor = {};
1433+ api.panelConstructor = {
1434+ themes: api.ThemesPanel
1435+ };
1436 api.sectionConstructor = {
1437 themes: api.ThemesSection
1438 };
1439Index: src/wp-includes/class-wp-customize-manager.php
1440===================================================================
1441--- src/wp-includes/class-wp-customize-manager.php (revision 38260)
1442+++ src/wp-includes/class-wp-customize-manager.php (working copy)
1443@@ -230,6 +230,7 @@
1444
1445 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
1446
1447+ require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
1448 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
1449 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
1450 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
1451@@ -289,6 +290,7 @@
1452
1453 add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
1454 add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
1455+ add_action( 'wp_ajax_customize-load-themes', array( $this, 'load_themes_ajax' ) );
1456
1457 add_action( 'customize_register', array( $this, 'register_controls' ) );
1458 add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
1459@@ -1634,6 +1636,9 @@
1460 foreach ( $this->controls as $control ) {
1461 $control->enqueue();
1462 }
1463+ if ( ! is_multisite() && current_user_can( 'install_themes') ) {
1464+ wp_enqueue_script( 'updates' );
1465+ }
1466 }
1467
1468 /**
1469@@ -1787,6 +1792,7 @@
1470 $nonces = array(
1471 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
1472 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
1473+ 'customize-themes' => wp_create_nonce( 'customize-themes' ),
1474 );
1475
1476 /**
1477@@ -1970,8 +1976,10 @@
1478
1479 /* Panel, Section, and Control Types */
1480 $this->register_panel_type( 'WP_Customize_Panel' );
1481+ $this->register_panel_type( 'WP_Customize_Themes_Panel' );
1482 $this->register_section_type( 'WP_Customize_Section' );
1483 $this->register_section_type( 'WP_Customize_Sidebar_Section' );
1484+ $this->register_section_type( 'WP_Customize_Themes_Section' );
1485 $this->register_control_type( 'WP_Customize_Color_Control' );
1486 $this->register_control_type( 'WP_Customize_Media_Control' );
1487 $this->register_control_type( 'WP_Customize_Upload_Control' );
1488@@ -1981,49 +1989,76 @@
1489 $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
1490 $this->register_control_type( 'WP_Customize_Theme_Control' );
1491
1492- /* Themes */
1493+ /* Themes (controls are loaded via ajax) */
1494
1495- $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
1496+ $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array(
1497 'title' => $this->theme()->display( 'Name' ),
1498+ 'description' => __( 'Once themes are installed, you can live-preview them on your site, customize them, and publish them. Browse available themes in the categories in this menu or upload a theme from a <code>.zip</code> file.' ),
1499 'capability' => 'switch_themes',
1500 'priority' => 0,
1501 ) ) );
1502
1503- // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
1504- $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
1505+ $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array(
1506+ 'title' => __( 'Installed' ),
1507+ 'action' => 'installed',
1508 'capability' => 'switch_themes',
1509+ 'panel' => 'themes',
1510+ 'priority' => 0,
1511 ) ) );
1512
1513- require_once( ABSPATH . 'wp-admin/includes/theme.php' );
1514+ $this->add_section( new WP_Customize_Themes_Section( $this, 'featured_themes', array(
1515+ 'title' => __( 'Featured' ),
1516+ 'action' => 'featured',
1517+ 'capability' => 'install_themes',
1518+ 'panel' => 'themes',
1519+ 'priority' => 5,
1520+ ) ) );
1521
1522- // Theme Controls.
1523+ $this->add_section( new WP_Customize_Themes_Section( $this, 'popular_themes', array(
1524+ 'title' => __( 'Popular' ),
1525+ 'action' => 'popular',
1526+ 'capability' => 'install_themes',
1527+ 'panel' => 'themes',
1528+ 'priority' => 10,
1529+ ) ) );
1530
1531- // Add a control for the active/original theme.
1532- if ( ! $this->is_theme_active() ) {
1533- $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
1534- $active_theme = current( $themes );
1535- $active_theme['isActiveTheme'] = true;
1536- $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
1537- 'theme' => $active_theme,
1538- 'section' => 'themes',
1539- 'settings' => 'active_theme',
1540- ) ) );
1541- }
1542+ $this->add_section( new WP_Customize_Themes_Section( $this, 'latest_themes', array(
1543+ 'title' => __( 'Latest' ),
1544+ 'action' => 'latest',
1545+ 'capability' => 'install_themes',
1546+ 'panel' => 'themes',
1547+ 'priority' => 15,
1548+ ) ) );
1549
1550- $themes = wp_prepare_themes_for_js();
1551- foreach ( $themes as $theme ) {
1552- if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
1553- continue;
1554- }
1555+ $this->add_section( new WP_Customize_Themes_Section( $this, 'favorites_themes', array(
1556+ 'title' => __( 'Favorites' ),
1557+ 'action' => 'favorites',
1558+ 'capability' => 'install_themes',
1559+ 'panel' => 'themes',
1560+ 'priority' => 20,
1561+ ) ) );
1562+
1563+ $this->add_section( new WP_Customize_Themes_Section( $this, 'feature_filter_themes', array(
1564+ 'title' => __( 'Feature Filter' ),
1565+ 'action' => 'feature_filter',
1566+ 'capability' => 'install_themes',
1567+ 'panel' => 'themes',
1568+ 'priority' => 25,
1569+ ) ) );
1570+
1571+ $this->add_section( new WP_Customize_Themes_Section( $this, 'search_themes', array(
1572+ 'title' => __( 'Search themes &hellip;' ),
1573+ 'action' => 'search',
1574+ 'capability' => 'install_themes',
1575+ 'panel' => 'themes',
1576+ 'priority' => 30,
1577+ ) ) );
1578+
1579+ // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
1580+ $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
1581+ 'capability' => 'switch_themes',
1582+ ) ) );
1583
1584- $theme_id = 'theme_' . $theme['id'];
1585- $theme['isActiveTheme'] = false;
1586- $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
1587- 'theme' => $theme,
1588- 'section' => 'themes',
1589- 'settings' => 'active_theme',
1590- ) ) );
1591- }
1592
1593 /* Site Identity */
1594
1595@@ -2330,6 +2365,141 @@
1596 }
1597
1598 /**
1599+ * Load themes into the theme browsing/installation UI.
1600+ *
1601+ * @since 4.7.0
1602+ * @access public
1603+ */
1604+ public function load_themes_ajax() {
1605+ check_ajax_referer( 'customize-themes', 'customize-themes-nonce' );
1606+
1607+ if ( ! current_user_can( 'switch_themes' ) ) {
1608+ wp_die( -1 );
1609+ }
1610+
1611+ if ( empty( $_POST['theme_action'] ) ) {
1612+ wp_send_json_error( 'missing_theme_action' );
1613+ }
1614+
1615+ if ( 'search' === $_POST['theme_action'] && ! array_key_exists( 'search', $_POST ) ) {
1616+ wp_send_json_error( 'empty_search' );
1617+ } elseif ( 'favorites' === $_POST['theme_action'] && ! array_key_exists( 'user', $_POST ) ) {
1618+ wp_send_json_error( 'empty_user' );
1619+ } elseif ( 'feature_filter' === $_POST['theme_action'] && ! array_key_exists( 'tags', $_POST ) ) {
1620+ wp_send_json_error( 'no_features' );
1621+ }
1622+
1623+ require_once( ABSPATH . 'wp-admin/includes/theme.php' );
1624+ if ( 'installed' === $_POST['theme_action'] ) {
1625+ $themes = array( 'themes' => wp_prepare_themes_for_js() );
1626+ foreach ( $themes['themes'] as &$theme ) {
1627+ $theme['type'] = 'installed';
1628+ // Set active based on customized theme.
1629+ if ( $_POST['customized_theme'] === $theme['id'] ) {
1630+ $theme['active'] = true;
1631+ } else {
1632+ $theme['active'] = false;
1633+ }
1634+ }
1635+ } else {
1636+ if ( ! current_user_can( 'install_themes' ) ) {
1637+ wp_die( -1 );
1638+ }
1639+
1640+ // Arguments for all queries.
1641+ $args = array(
1642+ 'per_page' => 100,
1643+ 'page' => absint( $_POST['page'] ),
1644+ 'fields' => array(
1645+ 'slug' => true,
1646+ 'screenshot' => true,
1647+ 'description' => true,
1648+ 'requires' => true,
1649+ 'rating' => true,
1650+ 'downloaded' => true,
1651+ 'downloadLink' => true,
1652+ 'last_updated' => true,
1653+ 'homepage' => true,
1654+ 'num_ratings' => true,
1655+ 'tags' => true,
1656+ )
1657+ );
1658+
1659+ // Specialized handling for each query.
1660+ switch ( $_POST['theme_action'] ) {
1661+ case 'search':
1662+ $args['search'] = wp_unslash( $_POST['search'] );
1663+ break;
1664+ case 'favorites':
1665+ $args['user'] = wp_unslash( $_POST['user'] );
1666+ case 'featured':
1667+ case 'popular':
1668+ $args['browse'] = $_POST['theme_action'];
1669+ break;
1670+ case 'latest':
1671+ $args['browse'] = 'new';
1672+ break;
1673+ case 'feature_filter':
1674+ $args['tag'] = wp_unslash( $_POST['tags'] );
1675+ break;
1676+ }
1677+
1678+ // Load themes from the .org API.
1679+ $themes = themes_api( 'query_themes', $args );
1680+ if ( is_wp_error( $themes ) ) {
1681+ wp_send_json_error();
1682+ }
1683+
1684+ // Prepare a list of installed themes to check against before the loop.
1685+ $installed_themes = array();
1686+ $wp_themes = wp_get_themes();
1687+ foreach ( $wp_themes as $theme ) {
1688+ $installed_themes[] = $theme->get_stylesheet();
1689+ }
1690+ $update_php = network_admin_url( 'update.php?action=install-theme' );
1691+ foreach ( $themes->themes as &$theme ) {
1692+ $theme->install_url = add_query_arg( array(
1693+ 'theme' => $theme->slug,
1694+ '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug )
1695+ ), $update_php );
1696+
1697+ $theme->name = wp_kses( $theme->name, $themes_allowedtags );
1698+ $theme->author = wp_kses( $theme->author, $themes_allowedtags );
1699+ $theme->version = wp_kses( $theme->version, $themes_allowedtags );
1700+ $theme->description = wp_kses( $theme->description, $themes_allowedtags );
1701+ $theme->tags = implode( ', ', $theme->tags );
1702+ $theme->stars = wp_star_rating( array( 'rating' => $theme->rating, 'type' => 'percent', 'number' => $theme->num_ratings, 'echo' => false ) );
1703+ $theme->num_ratings = number_format_i18n( $theme->num_ratings );
1704+ $theme->preview_url = set_url_scheme( $theme->preview_url );
1705+
1706+ // Handle themes that are already installed as installed themes.
1707+ if ( in_array( $theme->slug, $installed_themes ) ) {
1708+ $theme->type = 'installed';
1709+ } else {
1710+ $theme->type = $_POST['theme_action'];
1711+ }
1712+
1713+ // Set active based on customized theme.
1714+ if ( $_POST['customized_theme'] === $theme->slug ) {
1715+ $theme->active = true;
1716+ } else {
1717+ $theme->active = false;
1718+ }
1719+
1720+ // Map available theme properties to installed theme properties.
1721+ $theme->id = $theme->slug;
1722+ $theme->screenshot = array( $theme->screenshot_url );
1723+ $theme->authorAndUri = $theme->author;
1724+ unset( $theme->slug );
1725+ unset( $theme->screenshot_url );
1726+ unset( $theme->author );
1727+ }
1728+ }
1729+ wp_send_json_success( $themes );
1730+ }
1731+
1732+
1733+ /**
1734 * Callback for validating the header_textcolor value.
1735 *
1736 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
1737Index: src/wp-includes/customize/class-wp-customize-theme-control.php
1738===================================================================
1739--- src/wp-includes/customize/class-wp-customize-theme-control.php (revision 38260)
1740+++ src/wp-includes/customize/class-wp-customize-theme-control.php (working copy)
1741@@ -67,13 +67,13 @@
1742 $preview_url = esc_url( add_query_arg( 'theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
1743 $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url );
1744 ?>
1745- <# if ( data.theme.isActiveTheme ) { #>
1746- <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">
1747+ <# if ( data.theme.active ) { #>
1748+ <div class="theme active" tabindex="0" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
1749 <# } else { #>
1750- <div class="theme" tabindex="0" data-preview-url="<?php echo esc_attr( $preview_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
1751+ <div class="theme" tabindex="0" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
1752 <# } #>
1753
1754- <# if ( data.theme.screenshot[0] ) { #>
1755+ <# if ( data.theme.screenshot && data.theme.screenshot[0] ) { #>
1756 <div class="theme-screenshot">
1757 <img data-src="{{ data.theme.screenshot[0] }}" alt="" />
1758 </div>
1759@@ -81,27 +81,35 @@
1760 <div class="theme-screenshot blank"></div>
1761 <# } #>
1762
1763- <# if ( data.theme.isActiveTheme ) { #>
1764- <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Customize' ); ?></span>
1765- <# } else { #>
1766- <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Live Preview' ); ?></span>
1767+ <span class="more-details theme-details" id="{{ data.theme.id }}-action"><?php _e( 'Theme Details' ); ?></span>
1768+
1769+
1770+ <# if ( false == false && 'installed' === data.theme.type && data.theme.hasUpdate ) { // @todo patch updates.js to update themes in the customizer. #>
1771+ <div class="update-message notice inline notice-warning notice-alt" data-slug="{{ data.theme.id }}"><p><?php _e( 'New version available. <button class="button-link update-theme" type="button">Update now</button>' ); ?></p></div>
1772 <# } #>
1773
1774- <div class="theme-author"><?php printf( __( 'By %s' ), '{{ data.theme.author }}' ); ?></div>
1775-
1776- <# if ( data.theme.isActiveTheme ) { #>
1777+ <# if ( data.theme.active ) { #>
1778 <h3 class="theme-name" id="{{ data.theme.id }}-name">
1779 <?php
1780 /* translators: %s: theme name */
1781- printf( __( '<span>Active:</span> %s' ), '{{{ data.theme.name }}}' );
1782+ printf( __( '<span>Current:</span> %s' ), '{{ data.theme.name }}' );
1783 ?>
1784 </h3>
1785+ <div class="theme-actions">
1786+ <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></span>
1787+ <# } else if ( 'installed' === data.theme.type ) { #>
1788+ <h3 class="theme-name" id="{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
1789+ <div class="theme-actions">
1790+ <button type="button" class="button button-primary preview-theme" data-preview-url="<?php echo esc_attr( $preview_url ); ?>"><?php _e( 'Live Preview' ); ?></span>
1791+ </div>
1792+ <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
1793 <# } else { #>
1794- <h3 class="theme-name" id="{{ data.theme.id }}-name">{{{ data.theme.name }}}</h3>
1795+ <h3 class="theme-name" id="{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
1796 <div class="theme-actions">
1797- <button type="button" class="button theme-details"><?php _e( 'Theme Details' ); ?></button>
1798+ <button type="button" class="button theme-install" data-slug="{{ data.theme.id }}"><?php _e( 'Install' ); ?></button>
1799+ <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.theme.id }}" data-previewurl="<?php echo esc_attr( $preview_url ); ?>"><?php _e( 'Install & Preview' ); ?></button>
1800 </div>
1801- <# } #>
1802+ <# } #>
1803 </div>
1804 <?php
1805 }
1806Index: src/wp-includes/customize/class-wp-customize-themes-panel.php
1807===================================================================
1808--- src/wp-includes/customize/class-wp-customize-themes-panel.php (revision 0)
1809+++ src/wp-includes/customize/class-wp-customize-themes-panel.php (working copy)
1810@@ -0,0 +1,119 @@
1811+<?php
1812+/**
1813+ * Customize API: WP_Customize_Themes_Panel class
1814+ *
1815+ * @package WordPress
1816+ * @subpackage Customize
1817+ * @since 4.7.0
1818+ */
1819+
1820+/**
1821+ * Customize Themes Panel Class
1822+ *
1823+ * @since 4.7.0
1824+ *
1825+ * @see WP_Customize_Panel
1826+ */
1827+class WP_Customize_Themes_Panel extends WP_Customize_Panel {
1828+
1829+ /**
1830+ * Panel type.
1831+ *
1832+ * @since 4.7.0
1833+ * @access public
1834+ * @var string
1835+ */
1836+ public $type = 'themes';
1837+
1838+ /**
1839+ * An Underscore (JS) template for rendering this panel's container.
1840+ *
1841+ * The themes panel renders a custom panel heading with the current theme and a switch themes button.
1842+ *
1843+ * @see WP_Customize_Panel::print_template()
1844+ *
1845+ * @since 4.7.0
1846+ * @access protected
1847+ */
1848+ protected function render_template() {
1849+ ?>
1850+ <li id="accordion-section-{{ data.id }}" class="accordion-section control-panel-themes">
1851+ <h3 class="accordion-section-title">
1852+ <?php
1853+ if ( $this->manager->is_theme_active() ) {
1854+ echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> {{ data.title }}';
1855+ } else {
1856+ echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> {{ data.title }}';
1857+ }
1858+ ?>
1859+
1860+ <?php
1861+ if ( current_user_can( 'switch_themes' ) ) : ?>
1862+ <button type="button" class="button change-theme"><?php _ex( 'Change', 'theme' ); ?></button>
1863+ <?php endif; ?>
1864+ </h3>
1865+ <ul class="accordion-sub-container control-panel-content"></ul>
1866+ </li>
1867+ <?php
1868+ }
1869+
1870+ /**
1871+ * An Underscore (JS) template for this panel's content (but not its container).
1872+ *
1873+ * Class variables for this panel class are available in the `data` JS object;
1874+ * export custom variables by overriding WP_Customize_Panel::json().
1875+ *
1876+ * @since 4.7.0
1877+ * @access protected
1878+ *
1879+ * @see WP_Customize_Panel::print_template()
1880+ */
1881+ protected function content_template() {
1882+ ?>
1883+ <li class="panel-meta accordion-section customize-info">
1884+ <div class="current-theme">
1885+ <span class="preview-notice">
1886+ <?php
1887+ /* translators: %s: the site/panel title in the Customizer */
1888+ printf( __( 'Current Theme %s' ), '<strong class="panel-title">{{ data.title }}</strong>' );
1889+ ?>
1890+ </span>
1891+ <button type="button" class="button customize-theme" tabindex="-1"><?php _e( 'Customize' ); ?></button>
1892+ </div>
1893+ <h2><?php _e( 'Themes' ); ?></h2>
1894+ <?php if ( current_user_can( 'install_themes' ) ) : ?>
1895+ <button type="button" class="upload-toggle page-title-action" aria-expanded="false"><?php _e( 'Upload Theme' ); ?></button>
1896+ <?php endif; ?>
1897+ </li>
1898+ <li class="upload-theme">
1899+ <?php if ( current_user_can( 'install_themes' ) ) : ?>
1900+ <p class="install-help"><?php _e( 'If you have a theme in a <code>.zip</code> format, you may install it by uploading it here.' ); ?></p>
1901+ <div class="wp-upload-form">
1902+ <input type="file" id="themezip" name="themezip"><label class="screen-reader-text" for="themezip"><?php _e( 'Theme zip file' ); ?></label>
1903+ <button type="button" id="upload-theme-submit" class="button"><?php _e( 'Install Now' ); ?></button>
1904+ </div>
1905+ <?php endif; ?>
1906+ </div>
1907+ </li>
1908+ <li id="customize-container"></li><?php // Used as a full-screen overlay transition after clicking to preview a theme. ?>
1909+ <li class="customize-themes-notifications">
1910+ <div class="notice notice-warning customize-themes-unsaved-changes" style="display: none;"><p>
1911+ <?php _e( 'You have unsaved changes that will be lost if you preview a new theme.' ); ?>
1912+ <button type="button" id="customize-themes-save" class="button" ><?php _e( 'Save & Publish' ); ?></button>
1913+ </p></div>
1914+ </li>
1915+ <li id="customize-theme-section-navigation" class="wp-filter">
1916+ <div class="filter-count"><span class="count theme-count">0</span></div>
1917+ <div class="filter-links"></div>
1918+ <# if ( data.description ) { #>
1919+ <button class="customize-help-toggle dashicons dashicons-editor-help" type="button" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
1920+ <div class="description customize-panel-description">
1921+ {{{ data.description }}}
1922+ </div>
1923+ <# } #>
1924+ </li>
1925+ <?php
1926+ }
1927+}
1928Index: src/wp-includes/customize/class-wp-customize-themes-section.php
1929===================================================================
1930--- src/wp-includes/customize/class-wp-customize-themes-section.php (revision 38260)
1931+++ src/wp-includes/customize/class-wp-customize-themes-section.php (working copy)
1932@@ -10,7 +10,7 @@
1933 /**
1934 * Customize Themes Section class.
1935 *
1936- * A UI container for theme controls, which behaves like a backwards Panel.
1937+ * A UI container for theme controls, which are displayed in tabbed sections.
1938 *
1939 * @since 4.2.0
1940 *
1941@@ -28,57 +28,96 @@
1942 public $type = 'themes';
1943
1944 /**
1945- * Render the themes section, which behaves like a panel.
1946+ * Theme section action.
1947 *
1948- * @since 4.2.0
1949+ * Defines the type of themes to load (installed, featured, latest, etc.).
1950+ *
1951+ * @since 4.7.0
1952+ * @access public
1953+ * @var string
1954+ */
1955+ public $action = '';
1956+
1957+ /**
1958+ * Get section parameters for JS.
1959+ *
1960+ * @since 4.7.0
1961+ * @access public
1962+ * @return array Exported parameters.
1963+ */
1964+ public function json() {
1965+ $exported = parent::json();
1966+ $exported['action'] = $this->action;
1967+
1968+ return $exported;
1969+ }
1970+
1971+ /**
1972+ * Render a themes section as a JS template.
1973+ *
1974+ * The template is only rendered by PHP once, so all actions are prepared at once on the server side.
1975+ *
1976+ * @since 4.7.0
1977 * @access protected
1978 */
1979- protected function render() {
1980- $classes = 'accordion-section control-section control-section-' . $this->type;
1981+ protected function render_template() {
1982 ?>
1983- <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>">
1984- <h3 class="accordion-section-title">
1985- <?php
1986- if ( $this->manager->is_theme_active() ) {
1987- echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title;
1988- } else {
1989- echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title;
1990- }
1991- ?>
1992-
1993- <?php if ( count( $this->controls ) > 0 ) : ?>
1994- <button type="button" class="button change-theme" tabindex="0"><?php _ex( 'Change', 'theme' ); ?></button>
1995- <?php endif; ?>
1996- </h3>
1997- <div class="customize-themes-panel control-panel-content themes-php">
1998- <h3 class="accordion-section-title customize-section-title">
1999- <span class="customize-action"><?php _e( 'Customizing' ); ?></span>
2000- <?php _e( 'Themes' ); ?>
2001- <span class="title-count theme-count"><?php echo count( $this->controls ) + 1 /* Active theme */; ?></span>
2002- </h3>
2003- <h3 class="accordion-section-title customize-section-title">
2004+ <li id="accordion-section-{{ data.id }}" class="theme-section">
2005+ <# if ( 'search' === data.action ) { #>
2006+ <div class="search-form customize-themes-section-title themes-section-search_themes">
2007+ <label class="screen-reader-text" for="wp-filter-search-input">{{ data.title }}</label>
2008+ <input placeholder="{{ data.title }}" type="search" aria-describedby="live-search-desc" id="wp-filter-search-input" class="wp-filter-search">
2009+ <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span>
2010+ </div>
2011+ <# } else { #>
2012+ <button type="button" class="customize-themes-section-title themes-section-{{ data.id }}">{{ data.title }}</button>
2013+ <# } #>
2014+ <div class="customize-themes-section control-section-content themes-php">
2015+ <# if ( 'favorites' === data.action ) { #>
2016+ <div class="favorites-form">
2017+ <p class="install-help"><?php _e( 'If you have marked themes as favorites on WordPress.org, you can browse them here.' ); ?></p>
2018+ <p>
2019+ <label for="wporg-username-input"><?php _e( 'Your WordPress.org username:' ); ?></label>
2020+ <input type="search" id="wporg-username-input" value="">
2021+ <button type="button" class="button button-secondary favorites-form-submit"><?php _e( 'Get Favorites' ); ?></button>
2022+ </p>
2023+ </div>
2024+ <# } else if ( 'feature_filter' === data.action ) { #>
2025+ <div class="filter-drawer">
2026+ <div class="buttons">
2027+ <button type="button" class="apply-filters button button-secondary"><?php _e( 'Apply Filters' ); ?><span></span></a>
2028+ <button type="button" class="clear-filters button button-secondary"><?php _e( 'Clear' ); ?></a>
2029+ </div>
2030 <?php
2031- if ( $this->manager->is_theme_active() ) {
2032- echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title;
2033- } else {
2034- echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title;
2035+ $feature_list = get_theme_feature_list();
2036+ foreach ( $feature_list as $feature_name => $features ) {
2037+ echo '<fieldset class="filter-group">';
2038+ $feature_name = esc_html( $feature_name );
2039+ echo '<legend>' . $feature_name . '</legend>';
2040+ echo '<div class="filter-group-feature">';
2041+ foreach ( $features as $feature => $feature_name ) {
2042+ $feature = esc_attr( $feature );
2043+ echo '<input type="checkbox" id="filter-id-' . $feature . '" value="' . $feature . '" /> ';
2044+ echo '<label for="filter-id-' . $feature . '">' . $feature_name . '</label><br>';
2045+ }
2046+ echo '</div>';
2047+ echo '</fieldset>';
2048 }
2049 ?>
2050- <button type="button" class="button customize-theme"><?php _e( 'Customize' ); ?></button>
2051- </h3>
2052-
2053+ <div class="filtered-by">
2054+ <span><?php _e( 'Filtering by:' ); ?></span>
2055+ <div class="tags"></div>
2056+ <button type="button" class="button-link"><?php _e( 'Edit' ); ?></a>
2057+ </div>
2058+ </div>
2059+ <# } #>
2060 <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
2061-
2062- <div id="customize-container"></div>
2063- <?php if ( count( $this->controls ) > 4 ) : ?>
2064- <p><label for="themes-filter">
2065- <span class="screen-reader-text"><?php _e( 'Search installed themes&hellip;' ); ?></span>
2066- <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search installed themes&hellip;' ); ?>" />
2067- </label></p>
2068- <?php endif; ?>
2069 <div class="theme-browser rendered">
2070- <ul class="themes accordion-section-content">
2071+ <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>
2072+ <ul class="themes">
2073 </ul>
2074+ <p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p>
2075+ <p class="spinner"></p>
2076 </div>
2077 </div>
2078 </li>