Improve/introduce Customizer JavaScript models for Controls, Sections, and Panels.

* Introduce models for panels and sections.
* Introduce API to expand and focus a control, section or panel.
* Allow deep-linking to panels, sections, and controls inside of the Customizer.
* Clean up `accordion.js`, removing all Customizer-specific logic.
* Add initial unit tests for `wp.customize.Class` in `customize-base.js`.

https://make.wordpress.org/core/2014/10/27/toward-a-complete-javascript-api-for-the-customizer/ provides an overview of how to use the JavaScript API.

props westonruter, celloexpressions, ryankienstra.
see #28032, #28579, #28580, #28650, #28709, #29758.
fixes #29529.


Built from https://develop.svn.wordpress.org/trunk@30102


git-svn-id: http://core.svn.wordpress.org/trunk@30102 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Dominik Schilling 2014-10-29 22:51:22 +00:00
parent 63cf4db8ae
commit 3c962ee5d8
15 changed files with 1387 additions and 297 deletions

View File

@ -53,8 +53,6 @@ do_action( 'customize_controls_init' );
wp_enqueue_script( 'customize-controls' ); wp_enqueue_script( 'customize-controls' );
wp_enqueue_style( 'customize-controls' ); wp_enqueue_style( 'customize-controls' );
wp_enqueue_script( 'accordion' );
/** /**
* Enqueue Customizer control scripts. * Enqueue Customizer control scripts.
* *
@ -130,7 +128,7 @@ do_action( 'customize_controls_print_scripts' );
?> ?>
<div id="widgets-right"><!-- For Widget Customizer, many widgets try to look for instances under div#widgets-right, so we have to add that ID to a container div in the Customizer for compat --> <div id="widgets-right"><!-- For Widget Customizer, many widgets try to look for instances under div#widgets-right, so we have to add that ID to a container div in the Customizer for compat -->
<div class="wp-full-overlay-sidebar-content accordion-container" tabindex="-1"> <div class="wp-full-overlay-sidebar-content" tabindex="-1">
<div id="customize-info" class="accordion-section <?php if ( $cannot_expand ) echo ' cannot-expand'; ?>"> <div id="customize-info" class="accordion-section <?php if ( $cannot_expand ) echo ' cannot-expand'; ?>">
<div class="accordion-section-title" aria-label="<?php esc_attr_e( 'Customizer Options' ); ?>" tabindex="0"> <div class="accordion-section-title" aria-label="<?php esc_attr_e( 'Customizer Options' ); ?>" tabindex="0">
<span class="preview-notice"><?php <span class="preview-notice"><?php
@ -160,13 +158,9 @@ do_action( 'customize_controls_print_scripts' );
<?php endif; ?> <?php endif; ?>
</div> </div>
<div id="customize-theme-controls"><ul> <div id="customize-theme-controls">
<?php <ul><?php // Panels and sections are managed here via JavaScript ?></ul>
foreach ( $wp_customize->containers() as $container ) { </div>
$container->maybe_render();
}
?>
</ul></div>
</div> </div>
</div> </div>
@ -252,10 +246,13 @@ do_action( 'customize_controls_print_scripts' );
), ),
'settings' => array(), 'settings' => array(),
'controls' => array(), 'controls' => array(),
'panels' => array(),
'sections' => array(),
'nonce' => array( 'nonce' => array(
'save' => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ), 'save' => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ),
'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() ) 'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() )
), ),
'autofocus' => array(),
); );
// Prepare Customize Setting objects to pass to Javascript. // Prepare Customize Setting objects to pass to Javascript.
@ -266,10 +263,32 @@ do_action( 'customize_controls_print_scripts' );
); );
} }
// Prepare Customize Control objects to pass to Javascript. // Prepare Customize Control objects to pass to JavaScript.
foreach ( $wp_customize->controls() as $id => $control ) { foreach ( $wp_customize->controls() as $id => $control ) {
$control->to_json(); $settings['controls'][ $id ] = $control->json();
$settings['controls'][ $id ] = $control->json; }
// Prepare Customize Section objects to pass to JavaScript.
foreach ( $wp_customize->sections() as $id => $section ) {
$settings['sections'][ $id ] = $section->json();
}
// Prepare Customize Panel objects to pass to JavaScript.
foreach ( $wp_customize->panels() as $id => $panel ) {
$settings['panels'][ $id ] = $panel->json();
foreach ( $panel->sections as $section_id => $section ) {
$settings['sections'][ $section_id ] = $section->json();
}
}
// Pass to frontend the Customizer construct being deeplinked
if ( isset( $_GET['autofocus'] ) && is_array( $_GET['autofocus'] ) ) {
$autofocus = wp_unslash( $_GET['autofocus'] );
foreach ( $autofocus as $type => $id ) {
if ( isset( $settings[ $type . 's' ][ $id ] ) ) {
$settings['autofocus'][ $type ] = $id;
}
}
} }
?> ?>

View File

@ -25,9 +25,6 @@
* *
* Note that any appropriate tags may be used, as long as the above classes are present. * Note that any appropriate tags may be used, as long as the above classes are present.
* *
* In addition to the standard accordion behavior, this file includes JS for the
* Customizer's "Panel" functionality.
*
* @since 3.6.0. * @since 3.6.0.
*/ */
@ -46,20 +43,8 @@
accordionSwitch( $( this ) ); accordionSwitch( $( this ) );
}); });
// Go back to the top-level Customizer accordion.
$( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {
if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
return;
}
e.preventDefault(); // Keep this AFTER the key filter above
panelSwitch( $( '.current-panel' ) );
});
}); });
var sectionContent = $( '.accordion-section-content' );
/** /**
* Close the current accordion section and open a new one. * Close the current accordion section and open a new one.
* *
@ -69,75 +54,22 @@
function accordionSwitch ( el ) { function accordionSwitch ( el ) {
var section = el.closest( '.accordion-section' ), var section = el.closest( '.accordion-section' ),
siblings = section.closest( '.accordion-container' ).find( '.open' ), siblings = section.closest( '.accordion-container' ).find( '.open' ),
content = section.find( sectionContent ); content = section.find( '.accordion-section-content' );
// This section has no content and cannot be expanded. // This section has no content and cannot be expanded.
if ( section.hasClass( 'cannot-expand' ) ) { if ( section.hasClass( 'cannot-expand' ) ) {
return; return;
} }
// Slide into a sub-panel instead of accordioning (Customizer-specific).
if ( section.hasClass( 'control-panel' ) ) {
panelSwitch( section );
return;
}
if ( section.hasClass( 'open' ) ) { if ( section.hasClass( 'open' ) ) {
section.toggleClass( 'open' ); section.toggleClass( 'open' );
content.toggle( true ).slideToggle( 150 ); content.toggle( true ).slideToggle( 150 );
} else { } else {
siblings.removeClass( 'open' ); siblings.removeClass( 'open' );
siblings.find( sectionContent ).show().slideUp( 150 ); siblings.find( '.accordion-section-content' ).show().slideUp( 150 );
content.toggle( false ).slideToggle( 150 ); content.toggle( false ).slideToggle( 150 );
section.toggleClass( 'open' ); section.toggleClass( 'open' );
} }
} }
/**
* Slide into an accordion sub-panel.
*
* For the Customizer-specific panel functionality
*
* @param {Object} panel Title element or back button of the accordion panel to toggle.
* @since 4.0.0
*/
function panelSwitch( panel ) {
var position, scroll,
section = panel.closest( '.accordion-section' ),
overlay = section.closest( '.wp-full-overlay' ),
container = section.closest( '.accordion-container' ),
siblings = container.find( '.open' ),
topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
backBtn = overlay.find( '.control-panel-back' ),
panelTitle = section.find( '.accordion-section-title' ).first(),
content = section.find( '.control-panel-content' );
if ( section.hasClass( 'current-panel' ) ) {
section.toggleClass( 'current-panel' );
overlay.toggleClass( 'in-sub-panel' );
content.delay( 180 ).hide( 0, function() {
content.css( 'margin-top', 'inherit' ); // Reset
} );
topPanel.attr( 'tabindex', '0' );
backBtn.attr( 'tabindex', '-1' );
panelTitle.focus();
container.scrollTop( 0 );
} else {
// Close all open sections in any accordion level.
siblings.removeClass( 'open' );
siblings.find( sectionContent ).show().slideUp( 0 );
content.show( 0, function() {
position = content.offset().top;
scroll = container.scrollTop();
content.css( 'margin-top', ( 45 - position - scroll ) );
section.toggleClass( 'current-panel' );
overlay.toggleClass( 'in-sub-panel' );
container.scrollTop( 0 );
} );
topPanel.attr( 'tabindex', '-1' );
backBtn.attr( 'tabindex', '0' );
backBtn.focus();
}
}
})(jQuery); })(jQuery);

View File

@ -1 +1 @@
!function(a){function b(a){var b=a.closest(".accordion-section"),e=b.closest(".accordion-container").find(".open"),f=b.find(d);if(!b.hasClass("cannot-expand"))return b.hasClass("control-panel")?void c(b):void(b.hasClass("open")?(b.toggleClass("open"),f.toggle(!0).slideToggle(150)):(e.removeClass("open"),e.find(d).show().slideUp(150),f.toggle(!1).slideToggle(150),b.toggleClass("open")))}function c(a){var b,c,e=a.closest(".accordion-section"),f=e.closest(".wp-full-overlay"),g=e.closest(".accordion-container"),h=g.find(".open"),i=f.find("#customize-theme-controls > ul > .accordion-section > .accordion-section-title").add("#customize-info > .accordion-section-title"),j=f.find(".control-panel-back"),k=e.find(".accordion-section-title").first(),l=e.find(".control-panel-content");e.hasClass("current-panel")?(e.toggleClass("current-panel"),f.toggleClass("in-sub-panel"),l.delay(180).hide(0,function(){l.css("margin-top","inherit")}),i.attr("tabindex","0"),j.attr("tabindex","-1"),k.focus(),g.scrollTop(0)):(h.removeClass("open"),h.find(d).show().slideUp(0),l.show(0,function(){b=l.offset().top,c=g.scrollTop(),l.css("margin-top",45-b-c),e.toggleClass("current-panel"),f.toggleClass("in-sub-panel"),g.scrollTop(0)}),i.attr("tabindex","-1"),j.attr("tabindex","0"),j.focus())}a(document).ready(function(){a(".accordion-container").on("click keydown",".accordion-section-title",function(c){("keydown"!==c.type||13===c.which)&&(c.preventDefault(),b(a(this)))}),a("#customize-header-actions").on("click keydown",".control-panel-back",function(b){("keydown"!==b.type||13===b.which)&&(b.preventDefault(),c(a(".current-panel")))})});var d=a(".accordion-section-content")}(jQuery); !function(a){function b(a){var b=a.closest(".accordion-section"),c=b.closest(".accordion-container").find(".open"),d=b.find(".accordion-section-content");b.hasClass("cannot-expand")||(b.hasClass("open")?(b.toggleClass("open"),d.toggle(!0).slideToggle(150)):(c.removeClass("open"),c.find(".accordion-section-content").show().slideUp(150),d.toggle(!1).slideToggle(150),b.toggleClass("open")))}a(document).ready(function(){a(".accordion-container").on("click keydown",".accordion-section-title",function(c){("keydown"!==c.type||13===c.which)&&(c.preventDefault(),b(a(this)))})})}(jQuery);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -404,6 +404,23 @@
* @augments wp.customize.Control * @augments wp.customize.Control
*/ */
api.Widgets.WidgetControl = api.Control.extend({ api.Widgets.WidgetControl = api.Control.extend({
defaultExpandedArguments: {
duration: 'fast'
},
initialize: function ( id, options ) {
var control = this;
api.Control.prototype.initialize.call( control, id, options );
control.expanded = new api.Value();
control.expandedArgumentsQueue = [];
control.expanded.bind( function ( expanded ) {
var args = control.expandedArgumentsQueue.shift();
args = $.extend( {}, control.defaultExpandedArguments, args );
control.onChangeExpanded( expanded, args );
});
control.expanded.set( false );
},
/** /**
* Set up the control * Set up the control
*/ */
@ -529,13 +546,13 @@
if ( sidebarWidgetsControl.isReordering ) { if ( sidebarWidgetsControl.isReordering ) {
return; return;
} }
self.toggleForm(); self.expanded( ! self.expanded() );
} ); } );
$closeBtn = this.container.find( '.widget-control-close' ); $closeBtn = this.container.find( '.widget-control-close' );
$closeBtn.on( 'click', function( e ) { $closeBtn.on( 'click', function( e ) {
e.preventDefault(); e.preventDefault();
self.collapseForm(); self.collapse();
self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
} ); } );
}, },
@ -777,9 +794,14 @@
* Overrides api.Control.toggle() * Overrides api.Control.toggle()
* *
* @param {Boolean} active * @param {Boolean} active
* @param {Object} args
*/ */
toggle: function ( active ) { onChangeActive: function ( active, args ) {
// Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
this.container.toggleClass( 'widget-rendered', active ); this.container.toggleClass( 'widget-rendered', active );
if ( args.completeCallback ) {
args.completeCallback();
}
}, },
/** /**
@ -1101,51 +1123,90 @@
* Expand the accordion section containing a control * Expand the accordion section containing a control
*/ */
expandControlSection: function() { expandControlSection: function() {
var $section = this.container.closest( '.accordion-section' ); api.Control.prototype.expand.call( this );
if ( ! $section.hasClass( 'open' ) ) {
$section.find( '.accordion-section-title:first' ).trigger( 'click' );
}
}, },
/**
* @param {Boolean} expanded
* @param {Object} [params]
* @returns {Boolean} false if state already applied
*/
_toggleExpanded: api.Section.prototype._toggleExpanded,
/**
* @param {Object} [params]
* @returns {Boolean} false if already expanded
*/
expand: api.Section.prototype.expand,
/** /**
* Expand the widget form control * Expand the widget form control
*
* @deprecated alias of expand()
*/ */
expandForm: function() { expandForm: function() {
this.toggleForm( true ); this.expand();
}, },
/**
* @param {Object} [params]
* @returns {Boolean} false if already collapsed
*/
collapse: api.Section.prototype.collapse,
/** /**
* Collapse the widget form control * Collapse the widget form control
*
* @deprecated alias of expand()
*/ */
collapseForm: function() { collapseForm: function() {
this.toggleForm( false ); this.collapse();
}, },
/** /**
* Expand or collapse the widget control * Expand or collapse the widget control
* *
* @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
*
* @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
*/ */
toggleForm: function( showOrHide ) { toggleForm: function( showOrHide ) {
var self = this, $widget, $inside, complete;
$widget = this.container.find( 'div.widget:first' );
$inside = $widget.find( '.widget-inside:first' );
if ( typeof showOrHide === 'undefined' ) { if ( typeof showOrHide === 'undefined' ) {
showOrHide = ! $inside.is( ':visible' ); showOrHide = ! this.expanded();
} }
this.expanded( showOrHide );
},
// Already expanded or collapsed, so noop /**
if ( $inside.is( ':visible' ) === showOrHide ) { * Respond to change in the expanded state.
*
* @param {Boolean} expanded
* @param {Object} args merged on top of this.defaultActiveArguments
*/
onChangeExpanded: function ( expanded, args ) {
var self = this, $widget, $inside, complete, prevComplete;
// If the expanded state is unchanged only manipulate container expanded states
if ( args.unchanged ) {
if ( expanded ) {
api.Control.prototype.expand.call( self, {
completeCallback: args.completeCallback
});
}
return; return;
} }
if ( showOrHide ) { $widget = this.container.find( 'div.widget:first' );
$inside = $widget.find( '.widget-inside:first' );
if ( expanded ) {
self.expandControlSection();
// Close all other widget controls before expanding this one // Close all other widget controls before expanding this one
api.control.each( function( otherControl ) { api.control.each( function( otherControl ) {
if ( self.params.type === otherControl.params.type && self !== otherControl ) { if ( self.params.type === otherControl.params.type && self !== otherControl ) {
otherControl.collapseForm(); otherControl.collapse();
} }
} ); } );
@ -1154,29 +1215,44 @@
self.container.addClass( 'expanded' ); self.container.addClass( 'expanded' );
self.container.trigger( 'expanded' ); self.container.trigger( 'expanded' );
}; };
if ( args.completeCallback ) {
prevComplete = complete;
complete = function () {
prevComplete();
args.completeCallback();
};
}
if ( self.params.is_wide ) { if ( self.params.is_wide ) {
$inside.fadeIn( 'fast', complete ); $inside.fadeIn( args.duration, complete );
} else { } else {
$inside.slideDown( 'fast', complete ); $inside.slideDown( args.duration, complete );
} }
self.container.trigger( 'expand' ); self.container.trigger( 'expand' );
self.container.addClass( 'expanding' ); self.container.addClass( 'expanding' );
} else { } else {
complete = function() { complete = function() {
self.container.removeClass( 'collapsing' ); self.container.removeClass( 'collapsing' );
self.container.removeClass( 'expanded' ); self.container.removeClass( 'expanded' );
self.container.trigger( 'collapsed' ); self.container.trigger( 'collapsed' );
}; };
if ( args.completeCallback ) {
prevComplete = complete;
complete = function () {
prevComplete();
args.completeCallback();
};
}
self.container.trigger( 'collapse' ); self.container.trigger( 'collapse' );
self.container.addClass( 'collapsing' ); self.container.addClass( 'collapsing' );
if ( self.params.is_wide ) { if ( self.params.is_wide ) {
$inside.fadeOut( 'fast', complete ); $inside.fadeOut( args.duration, complete );
} else { } else {
$inside.slideUp( 'fast', function() { $inside.slideUp( args.duration, function() {
$widget.css( { width:'', margin:'' } ); $widget.css( { width:'', margin:'' } );
complete(); complete();
} ); } );
@ -1184,16 +1260,6 @@
} }
}, },
/**
* Expand the containing sidebar section, expand the form, and focus on
* the first input in the control
*/
focus: function() {
this.expandControlSection();
this.expandForm();
this.container.find( '.widget-content :focusable:first' ).focus();
},
/** /**
* Get the position (index) of the widget in the containing sidebar * Get the position (index) of the widget in the containing sidebar
* *
@ -1304,6 +1370,7 @@
* @augments wp.customize.Control * @augments wp.customize.Control
*/ */
api.Widgets.SidebarControl = api.Control.extend({ api.Widgets.SidebarControl = api.Control.extend({
/** /**
* Set up the control * Set up the control
*/ */
@ -1325,7 +1392,7 @@
registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id ); registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id );
this.setting.bind( function( newWidgetIds, oldWidgetIds ) { this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds; var widgetFormControls, removedWidgetIds, priority;
removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
@ -1350,21 +1417,16 @@
widgetFormControls.sort( function( a, b ) { widgetFormControls.sort( function( a, b ) {
var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
return aIndex - bIndex;
});
if ( aIndex === bIndex ) { priority = 0;
return 0; _( widgetFormControls ).each( function ( control ) {
} control.priority( priority );
control.section( self.section() );
return aIndex < bIndex ? -1 : 1; priority += 1;
} ); });
self.priority( priority ); // Make sure sidebar control remains at end
// Append the controls to put them in the right order
finalControlContainers = _( widgetFormControls ).map( function( widgetFormControls ) {
return widgetFormControls.container[0];
} );
$sidebarWidgetsAddControl = self.$sectionContent.find( '.customize-control-sidebar_widgets' );
$sidebarWidgetsAddControl.before( finalControlContainers );
// Re-sort widget form controls (including widgets form other sidebars newly moved here) // Re-sort widget form controls (including widgets form other sidebars newly moved here)
self._applyCardinalOrderClassNames(); self._applyCardinalOrderClassNames();
@ -1434,36 +1496,9 @@
// Update the model with whether or not the sidebar is rendered // Update the model with whether or not the sidebar is rendered
self.active.bind( function ( active ) { self.active.bind( function ( active ) {
registeredSidebar.set( 'is_rendered', active ); registeredSidebar.set( 'is_rendered', active );
api.section( self.section.get() ).active( active );
} ); } );
}, api.section( self.section.get() ).active( self.active() );
/**
* Show the sidebar section when it becomes visible.
*
* Overrides api.Control.toggle()
*
* @param {Boolean} active
*/
toggle: function ( active ) {
var $section, sectionSelector;
sectionSelector = '#accordion-section-sidebar-widgets-' + this.params.sidebar_id;
$section = $( sectionSelector );
if ( active ) {
$section.stop().slideDown( function() {
$( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
} );
} else {
// Make sure that hidden sections get closed first
if ( $section.hasClass( 'open' ) ) {
// it would be nice if accordionSwitch() in accordion.js was public
$section.find( '.accordion-section-title' ).trigger( 'click' );
}
$section.stop().slideUp();
}
}, },
/** /**
@ -1500,12 +1535,18 @@
this.$controlSection.find( '.accordion-section-title' ).droppable({ this.$controlSection.find( '.accordion-section-title' ).droppable({
accept: '.customize-control-widget_form', accept: '.customize-control-widget_form',
over: function() { over: function() {
if ( ! self.$controlSection.hasClass( 'open' ) ) { var section = api.section( self.section.get() );
self.$controlSection.addClass( 'open' ); section.expand({
self.$sectionContent.toggle( false ).slideToggle( 150, function() { allowMultiple: true, // Prevent the section being dragged from to be collapsed
self.$sectionContent.sortable( 'refreshPositions' ); completeCallback: function () {
} ); // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed
} api.section.each( function ( otherSection ) {
if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
}
} );
}
});
} }
}); });
@ -1548,16 +1589,30 @@
* Add classes to the widget_form controls to assist with styling * Add classes to the widget_form controls to assist with styling
*/ */
_applyCardinalOrderClassNames: function() { _applyCardinalOrderClassNames: function() {
this.$sectionContent.find( '.customize-control-widget_form' ) var widgetControls = [];
.removeClass( 'first-widget' ) _.each( this.setting(), function ( widgetId ) {
.removeClass( 'last-widget' ) var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); if ( widgetControl ) {
widgetControls.push( widgetControl );
}
});
this.$sectionContent.find( '.customize-control-widget_form:first' ) if ( ! widgetControls.length ) {
return;
}
$( widgetControls ).each( function () {
$( this.container )
.removeClass( 'first-widget' )
.removeClass( 'last-widget' )
.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
});
_.first( widgetControls ).container
.addClass( 'first-widget' ) .addClass( 'first-widget' )
.find( '.move-widget-up' ).prop( 'tabIndex', -1 ); .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
this.$sectionContent.find( '.customize-control-widget_form:last' ) _.last( widgetControls ).container
.addClass( 'last-widget' ) .addClass( 'last-widget' )
.find( '.move-widget-down' ).prop( 'tabIndex', -1 ); .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
}, },
@ -1571,6 +1626,8 @@
* Enable/disable the reordering UI * Enable/disable the reordering UI
* *
* @param {Boolean} showOrHide to enable/disable reordering * @param {Boolean} showOrHide to enable/disable reordering
*
* @todo We should have a reordering state instead and rename this to onChangeReordering
*/ */
toggleReordering: function( showOrHide ) { toggleReordering: function( showOrHide ) {
showOrHide = Boolean( showOrHide ); showOrHide = Boolean( showOrHide );
@ -1584,7 +1641,7 @@
if ( showOrHide ) { if ( showOrHide ) {
_( this.getWidgetFormControls() ).each( function( formControl ) { _( this.getWidgetFormControls() ).each( function( formControl ) {
formControl.collapseForm(); formControl.collapse();
} ); } );
this.$sectionContent.find( '.first-widget .move-widget' ).focus(); this.$sectionContent.find( '.first-widget .move-widget' ).focus();
@ -1619,7 +1676,7 @@
* @returns {object|false} widget_form control instance, or false on error * @returns {object|false} widget_form control instance, or false on error
*/ */
addWidget: function( widgetId ) { addWidget: function( widgetId ) {
var self = this, controlHtml, $widget, controlType = 'widget_form', $control, controlConstructor, var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
parsedWidgetId = parseWidgetId( widgetId ), parsedWidgetId = parseWidgetId( widgetId ),
widgetNumber = parsedWidgetId.number, widgetNumber = parsedWidgetId.number,
widgetIdBase = parsedWidgetId.id_base, widgetIdBase = parsedWidgetId.id_base,
@ -1651,30 +1708,28 @@
$widget = $( controlHtml ); $widget = $( controlHtml );
$control = $( '<li/>' ) controlContainer = $( '<li/>' )
.addClass( 'customize-control' ) .addClass( 'customize-control' )
.addClass( 'customize-control-' + controlType ) .addClass( 'customize-control-' + controlType )
.append( $widget ); .append( $widget );
// Remove icon which is visible inside the panel // Remove icon which is visible inside the panel
$control.find( '> .widget-icon' ).remove(); controlContainer.find( '> .widget-icon' ).remove();
if ( widget.get( 'is_multi' ) ) { if ( widget.get( 'is_multi' ) ) {
$control.find( 'input[name="widget_number"]' ).val( widgetNumber ); controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
$control.find( 'input[name="multi_number"]' ).val( widgetNumber ); controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
} }
widgetId = $control.find( '[name="widget-id"]' ).val(); widgetId = controlContainer.find( '[name="widget-id"]' ).val();
$control.hide(); // to be slid-down below controlContainer.hide(); // to be slid-down below
settingId = 'widget_' + widget.get( 'id_base' ); settingId = 'widget_' + widget.get( 'id_base' );
if ( widget.get( 'is_multi' ) ) { if ( widget.get( 'is_multi' ) ) {
settingId += '[' + widgetNumber + ']'; settingId += '[' + widgetNumber + ']';
} }
$control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
this.container.after( $control );
// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget) // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
isExistingWidget = api.has( settingId ); isExistingWidget = api.has( settingId );
@ -1692,6 +1747,7 @@
settings: { settings: {
'default': settingId 'default': settingId
}, },
content: controlContainer,
sidebar_id: self.params.sidebar_id, sidebar_id: self.params.sidebar_id,
widget_id: widgetId, widget_id: widgetId,
widget_id_base: widget.get( 'id_base' ), widget_id_base: widget.get( 'id_base' ),
@ -1731,9 +1787,9 @@
this.setting( sidebarWidgets ); this.setting( sidebarWidgets );
} }
$control.slideDown( function() { controlContainer.slideDown( function() {
if ( isExistingWidget ) { if ( isExistingWidget ) {
widgetFormControl.expandForm(); widgetFormControl.expand();
widgetFormControl.updateWidget( { widgetFormControl.updateWidget( {
instance: widgetFormControl.setting(), instance: widgetFormControl.setting(),
complete: function( error ) { complete: function( error ) {

File diff suppressed because one or more lines are too long

View File

@ -74,6 +74,7 @@ class WP_Customize_Control {
public $input_attrs = array(); public $input_attrs = array();
/** /**
* @deprecated It is better to just call the json() method
* @access public * @access public
* @var array * @var array
*/ */
@ -218,9 +219,24 @@ class WP_Customize_Control {
} }
$this->json['type'] = $this->type; $this->json['type'] = $this->type;
$this->json['priority'] = $this->priority;
$this->json['active'] = $this->active();
$this->json['section'] = $this->section;
$this->json['content'] = $this->get_content();
$this->json['label'] = $this->label; $this->json['label'] = $this->label;
$this->json['description'] = $this->description; $this->json['description'] = $this->description;
$this->json['active'] = $this->active(); }
/**
* Get the data to export to the client via JSON.
*
* @since 4.1.0
*
* @return array
*/
public function json() {
$this->to_json();
return $this->json;
} }
/** /**
@ -243,6 +259,21 @@ class WP_Customize_Control {
return true; return true;
} }
/**
* Get the control's content for insertion into the Customizer pane.
*
* @since 4.1.0
*
* @return string
*/
public final function get_content() {
ob_start();
$this->maybe_render();
$template = trim( ob_get_contents() );
ob_end_clean();
return $template;
}
/** /**
* Check capabilities and render the control. * Check capabilities and render the control.
* *
@ -1073,6 +1104,7 @@ class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control {
/** /**
* Widget Area Customize Control Class * Widget Area Customize Control Class
* *
* @since 3.9.0
*/ */
class WP_Widget_Area_Customize_Control extends WP_Customize_Control { class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
public $type = 'sidebar_widgets'; public $type = 'sidebar_widgets';
@ -1114,6 +1146,8 @@ class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
/** /**
* Widget Form Customize Control Class * Widget Form Customize Control Class
*
* @since 3.9.0
*/ */
class WP_Widget_Form_Customize_Control extends WP_Customize_Control { class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
public $type = 'widget_form'; public $type = 'widget_form';

View File

@ -498,6 +498,8 @@ final class WP_Customize_Manager {
$settings = array( $settings = array(
'values' => array(), 'values' => array(),
'channel' => wp_unslash( $_POST['customize_messenger_channel'] ), 'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
'activePanels' => array(),
'activeSections' => array(),
'activeControls' => array(), 'activeControls' => array(),
); );
@ -511,6 +513,12 @@ final class WP_Customize_Manager {
foreach ( $this->settings as $id => $setting ) { foreach ( $this->settings as $id => $setting ) {
$settings['values'][ $id ] = $setting->js_value(); $settings['values'][ $id ] = $setting->js_value();
} }
foreach ( $this->panels as $id => $panel ) {
$settings['activePanels'][ $id ] = $panel->active();
}
foreach ( $this->sections as $id => $section ) {
$settings['activeSections'][ $id ] = $section->active();
}
foreach ( $this->controls as $id => $control ) { foreach ( $this->controls as $id => $control ) {
$settings['activeControls'][ $id ] = $control->active(); $settings['activeControls'][ $id ] = $control->active();
} }
@ -911,11 +919,11 @@ final class WP_Customize_Manager {
if ( ! $section->panel ) { if ( ! $section->panel ) {
// Top-level section. // Top-level section.
$sections[] = $section; $sections[ $section->id ] = $section;
} else { } else {
// This section belongs to a panel. // This section belongs to a panel.
if ( isset( $this->panels [ $section->panel ] ) ) { if ( isset( $this->panels [ $section->panel ] ) ) {
$this->panels[ $section->panel ]->sections[] = $section; $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
} }
} }
} }
@ -932,8 +940,8 @@ final class WP_Customize_Manager {
continue; continue;
} }
usort( $panel->sections, array( $this, '_cmp_priority' ) ); uasort( $panel->sections, array( $this, '_cmp_priority' ) );
$panels[] = $panel; $panels[ $panel->id ] = $panel;
} }
$this->panels = $panels; $this->panels = $panels;

View File

@ -82,6 +82,28 @@ class WP_Customize_Panel {
*/ */
public $sections; public $sections;
/**
* @since 4.1.0
* @access public
* @var string
*/
public $type;
/**
* Callback.
*
* @since 4.1.0
* @access public
*
* @see WP_Customize_Section::active()
*
* @var callable Callback is called with one argument, the instance of
* WP_Customize_Section, and returns bool to indicate whether
* the section is active (such as it relates to the URL
* currently being previewed).
*/
public $active_callback = '';
/** /**
* Constructor. * Constructor.
* *
@ -103,12 +125,69 @@ class WP_Customize_Panel {
$this->manager = $manager; $this->manager = $manager;
$this->id = $id; $this->id = $id;
if ( empty( $this->active_callback ) ) {
$this->active_callback = array( $this, 'active_callback' );
}
$this->sections = array(); // Users cannot customize the $sections array. $this->sections = array(); // Users cannot customize the $sections array.
return $this; return $this;
} }
/**
* Check whether panel is active to current Customizer preview.
*
* @since 4.1.0
* @access public
*
* @return bool Whether the panel is active to the current preview.
*/
public final function active() {
$panel = $this;
$active = call_user_func( $this->active_callback, $this );
/**
* Filter response of WP_Customize_Panel::active().
*
* @since 4.1.0
*
* @param bool $active Whether the Customizer panel is active.
* @param WP_Customize_Panel $panel WP_Customize_Panel instance.
*/
$active = apply_filters( 'customize_panel_active', $active, $panel );
return $active;
}
/**
* Default callback used when invoking WP_Customize_Panel::active().
*
* Subclasses can override this with their specific logic, or they may
* provide an 'active_callback' argument to the constructor.
*
* @since 4.1.0
* @access public
*
* @return bool Always true.
*/
public function active_callback() {
return true;
}
/**
* Gather the parameters passed to client JavaScript via JSON.
*
* @since 4.1.0
*
* @return array The array to be exported to the client as JSON
*/
public function json() {
$array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'type' ) );
$array['content'] = $this->get_content();
$array['active'] = $this->active();
return $array;
}
/** /**
* Checks required user capabilities and whether the theme has the * Checks required user capabilities and whether the theme has the
* feature support required by the panel. * feature support required by the panel.
@ -129,6 +208,21 @@ class WP_Customize_Panel {
return true; return true;
} }
/**
* Get the panel's content template for insertion into the Customizer pane.
*
* @since 4.1.0
*
* @return string
*/
public final function get_content() {
ob_start();
$this->maybe_render();
$template = trim( ob_get_contents() );
ob_end_clean();
return $template;
}
/** /**
* Check capabilities and render the panel. * Check capabilities and render the panel.
* *
@ -189,7 +283,7 @@ class WP_Customize_Panel {
*/ */
protected function render_content() { protected function render_content() {
?> ?>
<li class="accordion-section control-section<?php if ( empty( $this->description ) ) echo ' cannot-expand'; ?>"> <li class="panel-meta accordion-section control-section<?php if ( empty( $this->description ) ) { echo ' cannot-expand'; } ?>">
<div class="accordion-section-title" tabindex="0"> <div class="accordion-section-title" tabindex="0">
<span class="preview-notice"><?php <span class="preview-notice"><?php
/* translators: %s is the site/panel title in the Customizer */ /* translators: %s is the site/panel title in the Customizer */
@ -203,8 +297,5 @@ class WP_Customize_Panel {
<?php endif; ?> <?php endif; ?>
</li> </li>
<?php <?php
foreach ( $this->sections as $section ) {
$section->maybe_render();
}
} }
} }

View File

@ -91,6 +91,28 @@ class WP_Customize_Section {
*/ */
public $controls; public $controls;
/**
* @since 4.1.0
* @access public
* @var string
*/
public $type;
/**
* Callback.
*
* @since 4.1.0
* @access public
*
* @see WP_Customize_Section::active()
*
* @var callable Callback is called with one argument, the instance of
* WP_Customize_Section, and returns bool to indicate whether
* the section is active (such as it relates to the URL
* currently being previewed).
*/
public $active_callback = '';
/** /**
* Constructor. * Constructor.
* *
@ -105,18 +127,76 @@ class WP_Customize_Section {
public function __construct( $manager, $id, $args = array() ) { public function __construct( $manager, $id, $args = array() ) {
$keys = array_keys( get_object_vars( $this ) ); $keys = array_keys( get_object_vars( $this ) );
foreach ( $keys as $key ) { foreach ( $keys as $key ) {
if ( isset( $args[ $key ] ) ) if ( isset( $args[ $key ] ) ) {
$this->$key = $args[ $key ]; $this->$key = $args[ $key ];
}
} }
$this->manager = $manager; $this->manager = $manager;
$this->id = $id; $this->id = $id;
if ( empty( $this->active_callback ) ) {
$this->active_callback = array( $this, 'active_callback' );
}
$this->controls = array(); // Users cannot customize the $controls array. $this->controls = array(); // Users cannot customize the $controls array.
return $this; return $this;
} }
/**
* Check whether section is active to current Customizer preview.
*
* @since 4.1.0
* @access public
*
* @return bool Whether the section is active to the current preview.
*/
public final function active() {
$section = $this;
$active = call_user_func( $this->active_callback, $this );
/**
* Filter response of WP_Customize_Section::active().
*
* @since 4.1.0
*
* @param bool $active Whether the Customizer section is active.
* @param WP_Customize_Section $section WP_Customize_Section instance.
*/
$active = apply_filters( 'customize_section_active', $active, $section );
return $active;
}
/**
* Default callback used when invoking WP_Customize_Section::active().
*
* Subclasses can override this with their specific logic, or they may
* provide an 'active_callback' argument to the constructor.
*
* @since 4.1.0
* @access public
*
* @return bool Always true.
*/
public function active_callback() {
return true;
}
/**
* Gather the parameters passed to client JavaScript via JSON.
*
* @since 4.1.0
*
* @return array The array to be exported to the client as JSON
*/
public function json() {
$array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'panel', 'type' ) );
$array['content'] = $this->get_content();
$array['active'] = $this->active();
return $array;
}
/** /**
* Checks required user capabilities and whether the theme has the * Checks required user capabilities and whether the theme has the
* feature support required by the section. * feature support required by the section.
@ -126,23 +206,41 @@ class WP_Customize_Section {
* @return bool False if theme doesn't support the section or user doesn't have the capability. * @return bool False if theme doesn't support the section or user doesn't have the capability.
*/ */
public final function check_capabilities() { public final function check_capabilities() {
if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) ) if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) ) {
return false; return false;
}
if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) ) if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) ) {
return false; return false;
}
return true; return true;
} }
/**
* Get the section's content template for insertion into the Customizer pane.
*
* @since 4.1.0
*
* @return string
*/
public final function get_content() {
ob_start();
$this->maybe_render();
$template = trim( ob_get_contents() );
ob_end_clean();
return $template;
}
/** /**
* Check capabilities and render the section. * Check capabilities and render the section.
* *
* @since 3.4.0 * @since 3.4.0
*/ */
public final function maybe_render() { public final function maybe_render() {
if ( ! $this->check_capabilities() ) if ( ! $this->check_capabilities() ) {
return; return;
}
/** /**
* Fires before rendering a Customizer section. * Fires before rendering a Customizer section.
@ -172,9 +270,6 @@ class WP_Customize_Section {
*/ */
protected function render() { protected function render() {
$classes = 'control-section accordion-section'; $classes = 'control-section accordion-section';
if ( $this->panel ) {
$classes .= ' control-subsection';
}
?> ?>
<li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>"> <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>">
<h3 class="accordion-section-title" tabindex="0"> <h3 class="accordion-section-title" tabindex="0">
@ -183,12 +278,10 @@ class WP_Customize_Section {
</h3> </h3>
<ul class="accordion-section-content"> <ul class="accordion-section-content">
<?php if ( ! empty( $this->description ) ) : ?> <?php if ( ! empty( $this->description ) ) : ?>
<li><p class="description customize-section-description"><?php echo $this->description; ?></p></li> <li class="customize-section-description-container">
<p class="description customize-section-description"><?php echo $this->description; ?></p>
</li>
<?php endif; ?> <?php endif; ?>
<?php
foreach ( $this->controls as $control )
$control->maybe_render();
?>
</ul> </ul>
</li> </li>
<?php <?php

View File

@ -184,8 +184,9 @@ window.wp = window.wp || {};
to = this.validate( to ); to = this.validate( to );
// Bail if the sanitized value is null or unchanged. // Bail if the sanitized value is null or unchanged.
if ( null === to || _.isEqual( from, to ) ) if ( null === to || _.isEqual( from, to ) ) {
return this; return this;
}
this._value = to; this._value = to;
this._dirty = true; this._dirty = true;

View File

@ -107,6 +107,8 @@
}); });
preview.send( 'ready', { preview.send( 'ready', {
activePanels: api.settings.activePanels,
activeSections: api.settings.activeSections,
activeControls: api.settings.activeControls activeControls: api.settings.activeControls
} ); } );

View File

@ -1 +1 @@
!function(a,b){var c,d=wp.customize;c=function(a,b,c){var d;return function(){var e=arguments;c=c||this,clearTimeout(d),d=setTimeout(function(){d=null,a.apply(c,e)},b)}},d.Preview=d.Messenger.extend({initialize:function(a,e){var f=this;d.Messenger.prototype.initialize.call(this,a,e),this.body=b(document.body),this.body.on("click.preview","a",function(a){a.preventDefault(),f.send("scroll",0),f.send("url",b(this).prop("href"))}),this.body.on("submit.preview","form",function(a){a.preventDefault()}),this.window=b(window),this.window.on("scroll.preview",c(function(){f.send("scroll",f.window.scrollTop())},200)),this.bind("scroll",function(a){f.window.scrollTop(a)})}}),b(function(){if(d.settings=window._wpCustomizeSettings,d.settings){var a,c;a=new d.Preview({url:window.location.href,channel:d.settings.channel}),a.bind("settings",function(a){b.each(a,function(a,b){d.has(a)?d(a).set(b):d.create(a,b)})}),a.trigger("settings",d.settings.values),a.bind("setting",function(a){var b;a=a.slice(),(b=d(a.shift()))&&b.set.apply(b,a)}),a.bind("sync",function(c){b.each(c,function(b,c){a.trigger(b,c)}),a.send("synced")}),a.bind("active",function(){d.settings.nonce&&a.send("nonce",d.settings.nonce)}),a.send("ready",{activeControls:d.settings.activeControls}),c=b.map(["color","image","position_x","repeat","attachment"],function(a){return"background_"+a}),d.when.apply(d,c).done(function(a,c,d,e,f){var g,h=b(document.body),i=b("head"),j=b("#custom-background-css");g=function(){var g="";h.toggleClass("custom-background",!(!a()&&!c())),a()&&(g+="background-color: "+a()+";"),c()&&(g+='background-image: url("'+c()+'");',g+="background-position: top "+d()+";",g+="background-repeat: "+e()+";",g+="background-attachment: "+f()+";"),j.remove(),j=b('<style type="text/css" id="custom-background-css">body.custom-background { '+g+" }</style>").appendTo(i)},b.each(arguments,function(){this.bind(g)})})}})}(wp,jQuery); !function(a,b){var c,d=wp.customize;c=function(a,b,c){var d;return function(){var e=arguments;c=c||this,clearTimeout(d),d=setTimeout(function(){d=null,a.apply(c,e)},b)}},d.Preview=d.Messenger.extend({initialize:function(a,e){var f=this;d.Messenger.prototype.initialize.call(this,a,e),this.body=b(document.body),this.body.on("click.preview","a",function(a){a.preventDefault(),f.send("scroll",0),f.send("url",b(this).prop("href"))}),this.body.on("submit.preview","form",function(a){a.preventDefault()}),this.window=b(window),this.window.on("scroll.preview",c(function(){f.send("scroll",f.window.scrollTop())},200)),this.bind("scroll",function(a){f.window.scrollTop(a)})}}),b(function(){if(d.settings=window._wpCustomizeSettings,d.settings){var a,c;a=new d.Preview({url:window.location.href,channel:d.settings.channel}),a.bind("settings",function(a){b.each(a,function(a,b){d.has(a)?d(a).set(b):d.create(a,b)})}),a.trigger("settings",d.settings.values),a.bind("setting",function(a){var b;a=a.slice(),(b=d(a.shift()))&&b.set.apply(b,a)}),a.bind("sync",function(c){b.each(c,function(b,c){a.trigger(b,c)}),a.send("synced")}),a.bind("active",function(){d.settings.nonce&&a.send("nonce",d.settings.nonce)}),a.send("ready",{activePanels:d.settings.activePanels,activeSections:d.settings.activeSections,activeControls:d.settings.activeControls}),c=b.map(["color","image","position_x","repeat","attachment"],function(a){return"background_"+a}),d.when.apply(d,c).done(function(a,c,d,e,f){var g,h=b(document.body),i=b("head"),j=b("#custom-background-css");g=function(){var g="";h.toggleClass("custom-background",!(!a()&&!c())),a()&&(g+="background-color: "+a()+";"),c()&&(g+='background-image: url("'+c()+'");',g+="background-position: top "+d()+";",g+="background-repeat: "+e()+";",g+="background-attachment: "+f()+";"),j.remove(),j=b('<style type="text/css" id="custom-background-css">body.custom-background { '+g+" }</style>").appendTo(i)},b.each(arguments,function(){this.bind(g)})})}})}(wp,jQuery);

View File

@ -4,7 +4,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '4.1-alpha-30101'; $wp_version = '4.1-alpha-30102';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.