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_style( 'customize-controls' );
wp_enqueue_script( 'accordion' );
/**
* 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 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 class="accordion-section-title" aria-label="<?php esc_attr_e( 'Customizer Options' ); ?>" tabindex="0">
<span class="preview-notice"><?php
@@ -160,13 +158,9 @@ do_action( 'customize_controls_print_scripts' );
<?php endif; ?>
</div>
<div id="customize-theme-controls"><ul>
<?php
foreach ( $wp_customize->containers() as $container ) {
$container->maybe_render();
}
?>
</ul></div>
<div id="customize-theme-controls">
<ul><?php // Panels and sections are managed here via JavaScript ?></ul>
</div>
</div>
</div>
@@ -252,10 +246,13 @@ do_action( 'customize_controls_print_scripts' );
),
'settings' => array(),
'controls' => array(),
'panels' => array(),
'sections' => array(),
'nonce' => array(
'save' => wp_create_nonce( 'save-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.
@@ -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 ) {
$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.
*
* In addition to the standard accordion behavior, this file includes JS for the
* Customizer's "Panel" functionality.
*
* @since 3.6.0.
*/
@@ -46,20 +43,8 @@
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.
*
@@ -69,75 +54,22 @@
function accordionSwitch ( el ) {
var section = el.closest( '.accordion-section' ),
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.
if ( section.hasClass( 'cannot-expand' ) ) {
return;
}
// Slide into a sub-panel instead of accordioning (Customizer-specific).
if ( section.hasClass( 'control-panel' ) ) {
panelSwitch( section );
return;
}
if ( section.hasClass( 'open' ) ) {
section.toggleClass( 'open' );
content.toggle( true ).slideToggle( 150 );
} else {
siblings.removeClass( 'open' );
siblings.find( sectionContent ).show().slideUp( 150 );
siblings.find( '.accordion-section-content' ).show().slideUp( 150 );
content.toggle( false ).slideToggle( 150 );
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);

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
*/
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
*/
@@ -529,13 +546,13 @@
if ( sidebarWidgetsControl.isReordering ) {
return;
}
self.toggleForm();
self.expanded( ! self.expanded() );
} );
$closeBtn = this.container.find( '.widget-control-close' );
$closeBtn.on( 'click', function( e ) {
e.preventDefault();
self.collapseForm();
self.collapse();
self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
} );
},
@@ -777,9 +794,14 @@
* Overrides api.Control.toggle()
*
* @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 );
if ( args.completeCallback ) {
args.completeCallback();
}
},
/**
@@ -1101,51 +1123,90 @@
* Expand the accordion section containing a control
*/
expandControlSection: function() {
var $section = this.container.closest( '.accordion-section' );
if ( ! $section.hasClass( 'open' ) ) {
$section.find( '.accordion-section-title:first' ).trigger( 'click' );
}
api.Control.prototype.expand.call( this );
},
/**
* @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
*
* @deprecated alias of expand()
*/
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
*
* @deprecated alias of expand()
*/
collapseForm: function() {
this.toggleForm( false );
this.collapse();
},
/**
* 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
*/
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' ) {
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;
}
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
api.control.each( function( otherControl ) {
if ( self.params.type === otherControl.params.type && self !== otherControl ) {
otherControl.collapseForm();
otherControl.collapse();
}
} );
@@ -1154,29 +1215,44 @@
self.container.addClass( 'expanded' );
self.container.trigger( 'expanded' );
};
if ( args.completeCallback ) {
prevComplete = complete;
complete = function () {
prevComplete();
args.completeCallback();
};
}
if ( self.params.is_wide ) {
$inside.fadeIn( 'fast', complete );
$inside.fadeIn( args.duration, complete );
} else {
$inside.slideDown( 'fast', complete );
$inside.slideDown( args.duration, complete );
}
self.container.trigger( 'expand' );
self.container.addClass( 'expanding' );
} else {
complete = function() {
self.container.removeClass( 'collapsing' );
self.container.removeClass( 'expanded' );
self.container.trigger( 'collapsed' );
};
if ( args.completeCallback ) {
prevComplete = complete;
complete = function () {
prevComplete();
args.completeCallback();
};
}
self.container.trigger( 'collapse' );
self.container.addClass( 'collapsing' );
if ( self.params.is_wide ) {
$inside.fadeOut( 'fast', complete );
$inside.fadeOut( args.duration, complete );
} else {
$inside.slideUp( 'fast', function() {
$inside.slideUp( args.duration, function() {
$widget.css( { width:'', margin:'' } );
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
*
@@ -1304,6 +1370,7 @@
* @augments wp.customize.Control
*/
api.Widgets.SidebarControl = api.Control.extend({
/**
* Set up the control
*/
@@ -1325,7 +1392,7 @@
registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id );
this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;
var widgetFormControls, removedWidgetIds, priority;
removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
@@ -1350,21 +1417,16 @@
widgetFormControls.sort( function( a, b ) {
var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
return aIndex - bIndex;
});
if ( aIndex === bIndex ) {
return 0;
}
return aIndex < bIndex ? -1 : 1;
} );
// 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 );
priority = 0;
_( widgetFormControls ).each( function ( control ) {
control.priority( priority );
control.section( self.section() );
priority += 1;
});
self.priority( priority ); // Make sure sidebar control remains at end
// Re-sort widget form controls (including widgets form other sidebars newly moved here)
self._applyCardinalOrderClassNames();
@@ -1434,36 +1496,9 @@
// Update the model with whether or not the sidebar is rendered
self.active.bind( function ( active ) {
registeredSidebar.set( 'is_rendered', active );
api.section( self.section.get() ).active( 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();
}
api.section( self.section.get() ).active( self.active() );
},
/**
@@ -1500,12 +1535,18 @@
this.$controlSection.find( '.accordion-section-title' ).droppable({
accept: '.customize-control-widget_form',
over: function() {
if ( ! self.$controlSection.hasClass( 'open' ) ) {
self.$controlSection.addClass( 'open' );
self.$sectionContent.toggle( false ).slideToggle( 150, function() {
self.$sectionContent.sortable( 'refreshPositions' );
} );
}
var section = api.section( self.section.get() );
section.expand({
allowMultiple: true, // Prevent the section being dragged from to be collapsed
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
*/
_applyCardinalOrderClassNames: function() {
this.$sectionContent.find( '.customize-control-widget_form' )
.removeClass( 'first-widget' )
.removeClass( 'last-widget' )
.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
var widgetControls = [];
_.each( this.setting(), function ( widgetId ) {
var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
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' )
.find( '.move-widget-up' ).prop( 'tabIndex', -1 );
this.$sectionContent.find( '.customize-control-widget_form:last' )
_.last( widgetControls ).container
.addClass( 'last-widget' )
.find( '.move-widget-down' ).prop( 'tabIndex', -1 );
},
@@ -1571,6 +1626,8 @@
* Enable/disable the reordering UI
*
* @param {Boolean} showOrHide to enable/disable reordering
*
* @todo We should have a reordering state instead and rename this to onChangeReordering
*/
toggleReordering: function( showOrHide ) {
showOrHide = Boolean( showOrHide );
@@ -1584,7 +1641,7 @@
if ( showOrHide ) {
_( this.getWidgetFormControls() ).each( function( formControl ) {
formControl.collapseForm();
formControl.collapse();
} );
this.$sectionContent.find( '.first-widget .move-widget' ).focus();
@@ -1619,7 +1676,7 @@
* @returns {object|false} widget_form control instance, or false on error
*/
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 ),
widgetNumber = parsedWidgetId.number,
widgetIdBase = parsedWidgetId.id_base,
@@ -1651,30 +1708,28 @@
$widget = $( controlHtml );
$control = $( '<li/>' )
controlContainer = $( '<li/>' )
.addClass( 'customize-control' )
.addClass( 'customize-control-' + controlType )
.append( $widget );
// Remove icon which is visible inside the panel
$control.find( '> .widget-icon' ).remove();
controlContainer.find( '> .widget-icon' ).remove();
if ( widget.get( 'is_multi' ) ) {
$control.find( 'input[name="widget_number"]' ).val( widgetNumber );
$control.find( 'input[name="multi_number"]' ).val( widgetNumber );
controlContainer.find( 'input[name="widget_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' );
if ( widget.get( 'is_multi' ) ) {
settingId += '[' + widgetNumber + ']';
}
$control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
this.container.after( $control );
controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
isExistingWidget = api.has( settingId );
@@ -1692,6 +1747,7 @@
settings: {
'default': settingId
},
content: controlContainer,
sidebar_id: self.params.sidebar_id,
widget_id: widgetId,
widget_id_base: widget.get( 'id_base' ),
@@ -1731,9 +1787,9 @@
this.setting( sidebarWidgets );
}
$control.slideDown( function() {
controlContainer.slideDown( function() {
if ( isExistingWidget ) {
widgetFormControl.expandForm();
widgetFormControl.expand();
widgetFormControl.updateWidget( {
instance: widgetFormControl.setting(),
complete: function( error ) {

File diff suppressed because one or more lines are too long