diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index fdd5f3a6ad..9b5bbd2b9e 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -827,6 +827,176 @@ p.customize-section-description { float: right; } +/** + * Themes + */ +@-webkit-keyframes customize-reload { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@-moz-keyframes customize-reload { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@keyframes customize-reload { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +/* #customize-container is reused from customize-loader.js, hence the naming. */ +.wp-customizer .customize-loading #customize-container { + display: block; + -webkit-animation: customize-reload .75s; /* Can't use `transition` because `display` changes here. */ + -moz-animation: customize-reload .75s; + animation: customize-reload .75s; +} + +.customize-themes-panel { + display: none; + padding: 0 8px; + background: #f1f1f1; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; +} + +.control-section.open .customize-themes-panel { + display: block; +} + +#customize-theme-controls .customize-themes-panel .accordion-section-content { + background: transparent; + display: block; +} + +.customize-control.customize-control-theme { + margin-bottom: 8px; +} + +.wp-customizer .theme-browser .themes { + padding-bottom: 8px; +} + +.wp-customizer .theme-browser .theme { + margin: 0; + width: 100%; +} + +.wp-customizer .theme-browser .theme .theme-actions { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; + opacity: 1; +} + +#customize-controls h3.theme-name { + font-size: 15px; +} + +.wp-customizer .theme-browser .theme.active .theme-name { + padding-right: 15px; +} + +.wp-customizer #themes-filter { + width: 100%; +} + +/* Panel-like behavior */ +#accordion-section-themes .accordion-section-title:after { + content: "\f148"; +} + +.rtl #accordion-section-themes .accordion-section-title:after { + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} + +#customize-theme-controls .control-section.current-panel > h3.accordion-section-title { + left: 0; +} + +.customize-themes-panel.control-panel-content { + position: absolute; + left: -100%; + top: 0; + width: 100%; + border-top: 1px solid #ddd; +} + +.in-themes-panel #customize-info, +.in-themes-panel #customize-theme-controls > ul > .accordion-section { + left: 100%; +} + +.themes-panel-back:before { + top: 13px; + left: 14px; +} + +.in-themes-panel .themes-panel-back { + left: 0; +} + +.in-sub-panel .themes-panel-back { + display: none; +} + +.control-panel-back.themes-panel-back:before { + content: "\f345"; +} + +.rtl .control-panel-back.themes-panel-back:before { + content: "\f341"; +} + +/* Details View */ +.wp-customizer .theme-overlay { + display: none; +} + +.wp-customizer.modal-open .theme-overlay { + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 109; +} + +.wp-customizer .theme-overlay .theme-backdrop { + background: rgba( 238, 238, 238, 0.75 ); + position: fixed; + z-index: 110; +} + +.wp-customizer .theme-overlay .theme-wrap { + left: 90px; + right: 90px; + top: 45px; + bottom: 45px; + z-index: 120; + max-width: 1740px; /* To ensure that theme screenshots are not displayed larger than 880px wide. */ +} + +.wp-customizer .theme-overlay .theme-actions { + text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */ +} + +.modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content { + overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */ +} + +/* Small Screens */ +@media (max-width:850px), (max-height:472px) { + .wp-customizer .theme-overlay .theme-wrap { + left: 0; + right: 0; + top: 0; + bottom: 0; + } +} + /** Handle cheaters. */ body.cheatin { diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index f052fbc0bf..d6cec32ab1 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -136,40 +136,17 @@ do_action( 'customize_controls_print_scripts' ); - theme()->get_screenshot(); - $cannot_expand = ! ( $wp_customize->is_theme_active() || $screenshot || $wp_customize->theme()->get('Description') ); - ?> -
-
+
is_theme_active() ) { - /* translators: %s is the theme name in the Customize/Live Preview pane */ - echo sprintf( __( 'You are previewing %s' ), '' . $wp_customize->theme()->display('Name') . '' ); - } else { - /* translators: %s is the site/panel title in the Customize pane */ - echo sprintf( __( 'You are customizing %s' ), '' . get_bloginfo( 'name' ) . '' ); - } + echo sprintf( __( 'You are customizing %s' ), '' . get_bloginfo( 'name' ) . '' ); ?>
- -
- is_theme_active() ) : - if ( $screenshot ) : ?> - - - - theme()->get('Description') ): ?> -
theme()->display('Description'); ?>
- -
- +
@@ -246,7 +223,7 @@ do_action( 'customize_controls_print_scripts' ); 'url' => array( 'preview' => esc_url_raw( $url ? $url : home_url( '/' ) ), 'parent' => esc_url_raw( admin_url() ), - 'activated' => esc_url_raw( admin_url( 'themes.php?activated=true&previewed' ) ), + 'activated' => esc_url_raw( home_url( '/' ) ), 'ajax' => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ), 'allowed' => array_map( 'esc_url_raw', $allowed_urls ), 'isCrossDomain' => $cross_domain, diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php index 2984218bf8..3ab1732ca2 100644 --- a/src/wp-admin/includes/theme.php +++ b/src/wp-admin/includes/theme.php @@ -486,3 +486,58 @@ function wp_prepare_themes_for_js( $themes = null ) { $prepared_themes = apply_filters( 'wp_prepare_themes_for_js', $prepared_themes ); return array_values( $prepared_themes ); } + +/** + * Print JS templates for the theme-browsing UI in the Customizer. + * + * @since 4.2.0 + */ +function customize_themes_print_templates() { + ?> + + ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), + backBtn = overlay.find( '.themes-panel-back' ), + panelTitle = section.find( '.accordion-section-title' ).first(), + content = section.find( '.control-panel-content' ); + + if ( expanded ) { + + // Collapse any sibling sections/panels + api.section.each( function ( otherSection ) { + if ( otherSection !== panel ) { + otherSection.collapse( { duration: args.duration } ); + } + }); + api.panel.each( function ( otherPanel ) { + if ( panel !== otherPanel ) { + otherPanel.collapse( { duration: 0 } ); + } + }); + + content.show( 0, function() { + position = content.offset().top; + scroll = container.scrollTop(); + content.css( 'margin-top', ( 45 - position - scroll ) ); + section.addClass( 'current-panel' ); + overlay.addClass( 'in-themes-panel' ); + container.scrollTop( 0 ); + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + topPanel.attr( 'tabindex', '-1' ); + backBtn.attr( 'tabindex', '0' ); + backBtn.focus(); + } else { + siblings.removeClass( 'open' ); + section.removeClass( 'current-panel' ); + overlay.removeClass( 'in-themes-panel' ); + content.delay( 180 ).hide( 0, function() { + content.css( 'margin-top', 'inherit' ); // Reset + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + topPanel.attr( 'tabindex', '0' ); + backBtn.attr( 'tabindex', '-1' ); + panelTitle.focus(); + container.scrollTop( 0 ); + } + }, + + /** + * Advance the modal to the next theme. + * + * @since 4.2.0 + */ + nextTheme: function () { + var section = this; + if ( section.getNextTheme() ) { + section.showDetails( section.getNextTheme(), function() { + section.overlay.find( '.right' ).focus(); + } ); + } + }, + + /** + * Get the next theme model. + * + * @since 4.2.0 + */ + getNextTheme: function () { + var control, next; + control = api.control( 'theme_' + this.currentTheme ); + next = control.container.next( 'li.customize-control-theme' ); + if ( ! next.length ) { + return false; + } + next = next[0].id.replace( 'customize-control-', '' ); + control = api.control( next ); + + return control.params.theme; + }, + + /** + * Advance the modal to the previous theme. + * + * @since 4.2.0 + */ + previousTheme: function () { + var section = this; + if ( section.getPreviousTheme() ) { + section.showDetails( section.getPreviousTheme(), function() { + section.overlay.find( '.left' ).focus(); + } ); + } + }, + + /** + * Get the previous theme model. + * + * @since 4.2.0 + */ + getPreviousTheme: function () { + var control, previous; + control = api.control( 'theme_' + this.currentTheme ); + previous = control.container.prev( 'li.customize-control-theme' ); + if ( ! previous.length ) { + return false; + } + previous = previous[0].id.replace( 'customize-control-', '' ); + control = api.control( previous ); + + return control.params.theme; + }, + + /** + * Disable buttons when we're viewing the first or last theme. + * + * @since 4.2.0 + */ + updateLimits: function () { + if ( ! this.getNextTheme() ) { + this.overlay.find( '.right' ).addClass( 'disabled' ); + } + if ( ! this.getPreviousTheme() ) { + this.overlay.find( '.left' ).addClass( 'disabled' ); + } + }, + + /** + * Render & show the theme details for a given theme model. + * + * @since 4.2.0 + * + * @param {Object} theme + */ + showDetails: function ( theme, callback ) { + var section = this; + callback = callback || function(){}; + section.currentTheme = theme.id; + section.overlay.html( section.template( theme ) ) + .fadeIn( 'fast' ) + .focus(); + $( 'body' ).addClass( 'modal-open' ); + section.containFocus( section.overlay ); + section.updateLimits(); + callback(); + }, + + /** + * Close the theme details modal. + * + * @since 4.2.0 + */ + closeDetails: function ( theme ) { + $( 'body' ).removeClass( 'modal-open' ); + this.overlay.fadeOut( 'fast' ); + api.control( 'theme_' + this.currentTheme ).focus(); + }, + + /** + * Keep tab focus within the theme details modal. + * + * @since 4.2.0 + */ + containFocus: function( el ) { + var tabbables; + + el.on( 'keydown', function( event ) { + + // Return if it's not the tab key + // When navigating with prev/next focus is already handled + if ( 9 !== event.keyCode ) { + return; + } + + // uses jQuery UI to get the tabbable elements + tabbables = $( ':tabbable', el ); + + // Keep focus within the overlay + if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { + tabbables.first().focus(); + return false; + } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { + tabbables.last().focus(); + return false; + } + }); + } + }); + /** * @since 4.1.0 * @@ -1409,6 +1754,63 @@ }); + /** + * wp.customize.ThemeControl + * + * @constructor + * @augments wp.customize.Control + * @augments wp.customize.Class + */ + api.ThemeControl = api.Control.extend({ + + /** + * @since 4.2.0 + */ + ready: function() { + var control = this; + + // Bind details view trigger. + control.container.on( 'click keydown', '.theme', function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + + if ( 'button' === event.target.className ) { + return; + } + + api.section( control.section() ).showDetails( control.params.theme ); + }); + + control.container.on( 'click keydown', '.theme-actions .button', function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + + $( '.wp-full-overlay' ).addClass( 'customize-loading' ); + }); + }, + + /** + * Show or hide the theme based on the presence of the term in the title, description, and author. + * + * @since 4.2.0 + */ + filter: function( term ) { + var control = this, + haystack = control.params.theme.name + ' ' + + control.params.theme.description + ' ' + + control.params.theme.tags + ' ' + + control.params.theme.author; + haystack = haystack.toLowerCase().replace( '-', ' ' ); + if ( -1 !== haystack.search( term ) ) { + control.activate(); + } else { + control.deactivate(); + } + } + }); + // Change objects contained within the main customize object to Settings. api.defaultConstructor = api.Setting; @@ -1853,14 +2255,17 @@ }); api.controlConstructor = { - color: api.ColorControl, - upload: api.UploadControl, - image: api.ImageControl, - header: api.HeaderControl, - background: api.BackgroundControl + color: api.ColorControl, + upload: api.UploadControl, + image: api.ImageControl, + header: api.HeaderControl, + background: api.BackgroundControl, + theme: api.ThemeControl }; api.panelConstructor = {}; - api.sectionConstructor = {}; + api.sectionConstructor = { + themes: api.ThemesSection + }; $( function() { api.settings = window._wpCustomizeSettings; @@ -2273,6 +2678,9 @@ // Prompt user with AYS dialog if leaving the Customizer with unsaved changes $( window ).on( 'beforeunload', function () { if ( ! api.state( 'saved' )() ) { + var timeout = setTimeout( function() { + overlay.removeClass( 'customize-loading' ); + }, 1 ); return api.l10n.saveAlert; } } ); diff --git a/src/wp-includes/admin-bar.php b/src/wp-includes/admin-bar.php index 05db8a7fee..f35125ec68 100644 --- a/src/wp-includes/admin-bar.php +++ b/src/wp-includes/admin-bar.php @@ -650,14 +650,33 @@ function wp_admin_bar_comments_menu( $wp_admin_bar ) { function wp_admin_bar_appearance_menu( $wp_admin_bar ) { $wp_admin_bar->add_group( array( 'parent' => 'site-name', 'id' => 'appearance' ) ); - if ( current_user_can( 'switch_themes' ) || current_user_can( 'edit_theme_options' ) ) - $wp_admin_bar->add_menu( array( 'parent' => 'appearance', 'id' => 'themes', 'title' => __('Themes'), 'href' => admin_url('themes.php') ) ); - - if ( ! current_user_can( 'edit_theme_options' ) ) - return; - $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; $customize_url = add_query_arg( 'url', urlencode( $current_url ), wp_customize_url() ); + + if ( current_user_can( 'switch_themes' ) ) { + $wp_admin_bar->add_menu( array( + 'parent' => 'appearance', + 'id' => 'themes', + 'title' => __( 'Themes' ), + 'href' => admin_url( 'themes.php' ), + 'meta' => array( + 'class' => 'hide-if-customize', + ), + ) ); + + if ( current_user_can( 'customize' ) ) { + $wp_admin_bar->add_menu( array( + 'parent' => 'appearance', + 'id' => 'customize-themes', + 'title' => __( 'Themes' ), + 'href' => add_query_arg( urlencode( 'autofocus[section]' ), 'themes', $customize_url ), // urlencode() needed due to #16859 + 'meta' => array( + 'class' => 'hide-if-no-customize', + ), + ) ); + } + } + if ( current_user_can( 'customize' ) ) { $wp_admin_bar->add_menu( array( 'parent' => 'appearance', @@ -671,6 +690,10 @@ function wp_admin_bar_appearance_menu( $wp_admin_bar ) { add_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' ); } + if ( ! current_user_can( 'edit_theme_options' ) ) { + return; + } + if ( current_theme_supports( 'widgets' ) ) { $wp_admin_bar->add_menu( array( 'parent' => 'appearance', diff --git a/src/wp-includes/class-wp-customize-control.php b/src/wp-includes/class-wp-customize-control.php index 8e7a3c57c0..154c5c8107 100644 --- a/src/wp-includes/class-wp-customize-control.php +++ b/src/wp-includes/class-wp-customize-control.php @@ -1100,6 +1100,101 @@ class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control { } } +/** + * Customize Theme Control Class + * + * @package WordPress + * @subpackage Customize + * @since 4.2.0 + */ +class WP_Customize_Theme_Control extends WP_Customize_Control { + + public $type = 'theme'; + public $theme; + + /** + * Refresh the parameters passed to the JavaScript via JSON. + * + * @since 4.2.0 + * @uses WP_Customize_Control::to_json() + */ + public function to_json() { + parent::to_json(); + $this->json['theme'] = $this->theme; + } + + /** + * Don't render the control content from PHP, as it's rendered via JS on load. + * + * @since 4.2.0 + */ + public function render_content() {} + + /** + * Render a JS template for theme display. + * + * @since 4.2.0 + */ + public function content_template() { + ?> +
+ <# if ( data.theme.screenshot[0] ) { #> +
+ +
+ <# } else { #> +
+ <# } #> + +
+ + <# if ( data.theme.active ) { #> +

{{ data.theme.name }}

+ <# } else { #> +

{{ data.theme.name }}

+ <# } #> + + <# if ( ! data.theme.active ) { #> +
+ +
+ <# } #> +
+ + + register_control_type( 'WP_Customize_Upload_Control' ); $this->register_control_type( 'WP_Customize_Image_Control' ); $this->register_control_type( 'WP_Customize_Background_Image_Control' ); + $this->register_control_type( 'WP_Customize_Theme_Control' ); + + /* Themes */ + + $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array( + 'title' => sprintf( __( 'Theme: %s' ), $this->theme()->display('Name') ), + 'capability' => 'switch_themes', + 'priority' => 0, + ) ) ); + + // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience). + $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array( + 'capability' => 'switch_themes', + ) ) ); + + require_once( ABSPATH . 'wp-admin/includes/theme.php' ); + + // Theme Controls. + $themes = wp_prepare_themes_for_js(); + foreach ( $themes as $theme ) { + $theme_id = 'theme_' . $theme['id']; + $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array( + 'theme' => $theme, + 'section' => 'themes', + 'settings' => 'active_theme', + ) ) ); + } + + $this->add_control( new WP_Customize_New_Theme_Control( $this, 'add_theme', array( + 'section' => 'themes', + 'settings' => 'active_theme', + ) ) ); /* Site Title & Tagline */ diff --git a/src/wp-includes/class-wp-customize-section.php b/src/wp-includes/class-wp-customize-section.php index 7d65689e4e..9815bfc1a1 100644 --- a/src/wp-includes/class-wp-customize-section.php +++ b/src/wp-includes/class-wp-customize-section.php @@ -311,6 +311,57 @@ class WP_Customize_Section { } } +/** + * Customize Themes Section Class. + * + * A UI container for theme controls, which behaves like a backwards Panel. + * + * @package WordPress + * @subpackage Customize + * @since 4.2.0 + */ +class WP_Customize_Themes_Section extends WP_Customize_Section { + + public $type = 'themes'; + + /** + * Render the themes section, which behaves like a panel. + * + * @since 4.2.0 + */ + protected function render() { + $classes = 'accordion-section control-section control-section-' . $this->type; + ?> +
  • +

    + title ); ?> + +

    + +
    +

    + controls ) - 1; ?> + + + +

    + +
    + controls ) ) : ?> +

    + +
    +
      +
    +
    +
    +
  • +