diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index b35bc3b7d6..6b9d910f73 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -123,13 +123,17 @@ body { color: #0073aa; } -#customize-controls .customize-info .customize-panel-description { +#customize-controls .customize-info .customize-panel-description, +#customize-controls .no-widget-areas-rendered-notice { color: #555; display: none; background: #fff; padding: 12px 15px; border-top: 1px solid #ddd; } +#customize-controls .customize-info .customize-panel-description.open + .no-widget-areas-rendered-notice { + border-top: none; +} #customize-controls .customize-info .customize-panel-description p:first-child { margin-top: 0; diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js index e0e4a4d61c..b1e7ba6653 100644 --- a/src/wp-admin/js/customize-controls.js +++ b/src/wp-admin/js/customize-controls.js @@ -300,7 +300,7 @@ * @param {Object} args.completeCallback */ onChangeActive: function( active, args ) { - var duration, construct = this; + var duration, construct = this, expandedOtherPanel; if ( args.unchanged ) { if ( args.completeCallback ) { args.completeCallback(); @@ -309,6 +309,24 @@ } duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 ); + + if ( construct.extended( api.Panel ) ) { + // If this is a panel is not currently expanded but another panel is expanded, do not animate. + api.panel.each(function ( panel ) { + if ( panel !== construct && panel.expanded() ) { + expandedOtherPanel = panel; + duration = 0; + } + }); + + // Collapse any expanded sections inside of this panel first before deactivating. + if ( ! active ) { + _.each( construct.sections(), function( section ) { + section.collapse( { duration: 0 } ); + } ); + } + } + if ( ! $.contains( document, construct.container[0] ) ) { // jQuery.fn.slideUp is not hiding an element if it is not in the DOM construct.container.toggle( active ); @@ -329,6 +347,11 @@ construct.container.stop( true, true ).slideUp( duration, args.completeCallback ); } } + + // Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle. + if ( expandedOtherPanel ) { + expandedOtherPanel._recalculateTopMargin(); + } }, /** @@ -378,39 +401,48 @@ }, /** - * @param {Boolean} expanded - * @param {Object} [params] - * @returns {Boolean} false if state already applied + * Handle the toggle logic for expand/collapse. + * + * @param {Boolean} expanded - The new state to apply. + * @param {Object} [params] - Object containing options for expand/collapse. + * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete. + * @returns {Boolean} false if state already applied or active state is false */ - _toggleExpanded: function ( expanded, params ) { - var self = this; + _toggleExpanded: function( expanded, params ) { + var instance = this, previousCompleteCallback; params = params || {}; - var section = this, previousCompleteCallback = params.completeCallback; - params.completeCallback = function () { + previousCompleteCallback = params.completeCallback; + + // Short-circuit expand() if the instance is not active. + if ( expanded && ! instance.active() ) { + return false; + } + + params.completeCallback = function() { if ( previousCompleteCallback ) { - previousCompleteCallback.apply( section, arguments ); + previousCompleteCallback.apply( instance, arguments ); } if ( expanded ) { - section.container.trigger( 'expanded' ); + instance.container.trigger( 'expanded' ); } else { - section.container.trigger( 'collapsed' ); + instance.container.trigger( 'collapsed' ); } }; - if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) { + if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) { params.unchanged = true; - self.onChangeExpanded( self.expanded.get(), params ); + instance.onChangeExpanded( instance.expanded.get(), params ); return false; } else { params.unchanged = false; - this.expandedArgumentsQueue.push( params ); - this.expanded.set( expanded ); + instance.expandedArgumentsQueue.push( params ); + instance.expanded.set( expanded ); return true; } }, /** * @param {Object} [params] - * @returns {Boolean} false if already expanded + * @returns {Boolean} false if already expanded or if inactive. */ expand: function ( params ) { return this._toggleExpanded( true, params ); @@ -418,7 +450,7 @@ /** * @param {Object} [params] - * @returns {Boolean} false if already collapsed + * @returns {Boolean} false if already collapsed. */ collapse: function ( params ) { return this._toggleExpanded( false, params ); @@ -539,6 +571,13 @@ }; section.panel.bind( inject ); inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one + + section.deferred.embedded.done(function() { + // Fix the top margin after reflow. + api.bind( 'pane-contents-reflowed', _.debounce( function() { + section._recalculateTopMargin(); + }, 100 ) ); + }); }, /** @@ -646,13 +685,7 @@ // Fix the height after browser resize. $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) ); - // Fix the top margin after reflow. - api.bind( 'pane-contents-reflowed', _.debounce( function() { - var offset = ( content.offset().top - headerActionsHeight ); - if ( 0 < offset ) { - content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) ); - } - }, 100 ) ); + section._recalculateTopMargin(); }; } @@ -693,6 +726,25 @@ args.completeCallback(); } } + }, + + /** + * Recalculate the top margin. + * + * @since 4.4.0 + * @private + */ + _recalculateTopMargin: function() { + var section = this, content, offset, headerActionsHeight; + content = section.container.find( '.accordion-section-content' ); + if ( 0 === content.length ) { + return; + } + headerActionsHeight = $( '#customize-header-actions' ).height(); + offset = ( content.offset().top - headerActionsHeight ); + if ( 0 < offset ) { + content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) ); + } } }); @@ -1155,6 +1207,11 @@ parentContainer.append( panel.container ); panel.renderContent(); } + + api.bind( 'pane-contents-reflowed', _.debounce( function() { + panel._recalculateTopMargin(); + }, 100 ) ); + panel.deferred.embedded.resolve(); }, @@ -1253,7 +1310,7 @@ * @param {Boolean} expanded * @param {Object} args * @param {Boolean} args.unchanged - * @param {Callback} args.completeCallback + * @param {Function} args.completeCallback */ onChangeExpanded: function ( expanded, args ) { @@ -1268,14 +1325,14 @@ // Note: there is a second argument 'args' passed var position, scroll, panel = this, - section = panel.container.closest( '.accordion-section' ), // This is actually the panel. - overlay = section.closest( '.wp-full-overlay' ), - container = section.closest( '.wp-full-overlay-sidebar-content' ), + accordionSection = panel.container.closest( '.accordion-section' ), + overlay = accordionSection.closest( '.wp-full-overlay' ), + container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ), siblings = container.find( '.open' ), topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ), - backBtn = section.find( '.customize-panel-back' ), - panelTitle = section.find( '.accordion-section-title' ).first(), - content = section.find( '.control-panel-content' ), + backBtn = accordionSection.find( '.customize-panel-back' ), + panelTitle = accordionSection.find( '.accordion-section-title' ).first(), + content = accordionSection.find( '.control-panel-content' ), headerActionsHeight = $( '#customize-header-actions' ).height(); if ( expanded ) { @@ -1297,7 +1354,7 @@ position = content.offset().top; scroll = container.scrollTop(); content.css( 'margin-top', ( headerActionsHeight - position - scroll ) ); - section.addClass( 'current-panel' ); + accordionSection.addClass( 'current-panel' ); overlay.addClass( 'in-sub-panel' ); container.scrollTop( 0 ); if ( args.completeCallback ) { @@ -1307,14 +1364,10 @@ topPanel.attr( 'tabindex', '-1' ); backBtn.attr( 'tabindex', '0' ); backBtn.focus(); - - // Fix the top margin after reflow. - api.bind( 'pane-contents-reflowed', _.debounce( function() { - content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) ); - }, 100 ) ); + panel._recalculateTopMargin(); } else { siblings.removeClass( 'open' ); - section.removeClass( 'current-panel' ); + accordionSection.removeClass( 'current-panel' ); overlay.removeClass( 'in-sub-panel' ); content.delay( 180 ).hide( 0, function() { content.css( 'margin-top', 'inherit' ); // Reset @@ -1329,6 +1382,20 @@ } }, + /** + * Recalculate the top margin. + * + * @since 4.4.0 + * @private + */ + _recalculateTopMargin: function() { + var panel = this, headerActionsHeight, content, accordionSection; + headerActionsHeight = $( '#customize-header-actions' ).height(); + accordionSection = panel.container.closest( '.accordion-section' ); + content = accordionSection.find( '.control-panel-content' ); + content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) ); + }, + /** * Render the panel from its JS template, if it exists. * diff --git a/src/wp-admin/js/customize-widgets.js b/src/wp-admin/js/customize-widgets.js index bd257575ac..aa84c6a5d0 100644 --- a/src/wp-admin/js/customize-widgets.js +++ b/src/wp-admin/js/customize-widgets.js @@ -1505,6 +1505,77 @@ } } ); + /** + * wp.customize.Widgets.WidgetsPanel + * + * Customizer panel containing the widget area sections. + * + * @since 4.4.0 + */ + api.Widgets.WidgetsPanel = api.Panel.extend({ + + /** + * Add and manage the display of the no-rendered-areas notice. + * + * @since 4.4.0 + */ + ready: function () { + var panel = this; + + api.Panel.prototype.ready.call( panel ); + + panel.deferred.embedded.done(function() { + var panelMetaContainer, noRenderedAreasNotice, shouldShowNotice; + panelMetaContainer = panel.container.find( '.panel-meta' ); + noRenderedAreasNotice = $( '
', { + 'class': 'no-widget-areas-rendered-notice' + }); + noRenderedAreasNotice.append( $( '', { + text: l10n.noAreasRendered + } ) ); + panelMetaContainer.append( noRenderedAreasNotice ); + + shouldShowNotice = function() { + return ( 0 === _.filter( panel.sections(), function( section ) { + return section.active(); + } ).length ); + }; + + /* + * Set the initial visibility state for rendered notice. + * Update the visibility of the notice whenever a reflow happens. + */ + noRenderedAreasNotice.toggle( shouldShowNotice() ); + api.previewer.deferred.active.done( function () { + noRenderedAreasNotice.toggle( shouldShowNotice() ); + }); + api.bind( 'pane-contents-reflowed', function() { + var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0; + if ( shouldShowNotice() ) { + noRenderedAreasNotice.slideDown( duration ); + } else { + noRenderedAreasNotice.slideUp( duration ); + } + }); + }); + }, + + /** + * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas). + * + * This ensures that the widgets panel appears even when there are no + * sidebars displayed on the URL currently being previewed. + * + * @since 4.4.0 + * + * @returns {boolean} + */ + isContextuallyActive: function() { + var panel = this; + return panel.active(); + } + }); + /** * wp.customize.Widgets.SidebarSection * @@ -1968,7 +2039,10 @@ } } ); - // Register models for custom section and control types + // Register models for custom panel, section, and control types + $.extend( api.panelConstructor, { + widgets: api.Widgets.WidgetsPanel + }); $.extend( api.sectionConstructor, { sidebar: api.Widgets.SidebarSection }); diff --git a/src/wp-includes/class-wp-customize-widgets.php b/src/wp-includes/class-wp-customize-widgets.php index 7b73dc2f15..c1a8e5ebd2 100644 --- a/src/wp-includes/class-wp-customize-widgets.php +++ b/src/wp-includes/class-wp-customize-widgets.php @@ -355,9 +355,11 @@ final class WP_Customize_Widgets { } $this->manager->add_panel( 'widgets', array( - 'title' => __( 'Widgets' ), - 'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ), - 'priority' => 110, + 'type' => 'widgets', + 'title' => __( 'Widgets' ), + 'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ), + 'priority' => 110, + 'active_callback' => array( $this, 'is_panel_active' ), ) ); foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) { @@ -454,6 +456,22 @@ final class WP_Customize_Widgets { add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 ); } + /** + * Return whether the widgets panel is active, based on whether there are sidebars registered. + * + * @since 4.4.0 + * @access public + * + * @see WP_Customize_Panel::$active_callback + * + * @global array $wp_registered_sidebars + * @return bool Active. + */ + public function is_panel_active() { + global $wp_registered_sidebars; + return ! empty( $wp_registered_sidebars ); + } + /** * Covert a widget_id into its corresponding Customizer setting ID (option name). * @@ -655,6 +673,7 @@ final class WP_Customize_Widgets { 'error' => __( 'An error has occurred. Please reload the page and try again.' ), 'widgetMovedUp' => __( 'Widget moved up' ), 'widgetMovedDown' => __( 'Widget moved down' ), + 'noAreasRendered' => __( 'There are no widget areas currently rendered in the preview. Navigate in the preview to a template that makes use of a widget area in order to access its widgets here.' ), ), 'tpl' => array( 'widgetReorderNav' => $widget_reorder_nav_tpl, diff --git a/tests/qunit/fixtures/customize-widgets.js b/tests/qunit/fixtures/customize-widgets.js index d3f8bb0326..e94fc002db 100644 --- a/tests/qunit/fixtures/customize-widgets.js +++ b/tests/qunit/fixtures/customize-widgets.js @@ -64,7 +64,7 @@ window._wpCustomizeSettings.panels.widgets = { 'id': 'widgets', 'description': 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).', 'priority': 110, - 'type': 'default', + 'type': 'widgets', 'title': 'Widgets', 'content': '', 'active': true, diff --git a/tests/qunit/wp-admin/js/customize-controls.js b/tests/qunit/wp-admin/js/customize-controls.js index 6ca4908499..91a7d36d08 100644 --- a/tests/qunit/wp-admin/js/customize-controls.js +++ b/tests/qunit/wp-admin/js/customize-controls.js @@ -393,7 +393,7 @@ jQuery( window ).load( function (){ panelId = 'mockPanelId'; panelTitle = 'Mock Panel Title'; panelDescription = 'Mock panel description'; - panelContent = '