l10n;
api.Widgets = api.Widgets || {};
+ api.Widgets.savedWidgetIds = {};
// Link settings
api.Widgets.data = _wpCustomizeWidgetsSettings || {};
/**
* @since 4.1.0
*/
- initialize: function ( id, options ) {
+ initialize: function( id, options ) {
var control = this;
- api.Control.prototype.initialize.call( control, id, options );
- control.expanded = new api.Value();
+
+ control.widgetControlEmbedded = false;
+ control.widgetContentEmbedded = false;
+ control.expanded = new api.Value( false );
control.expandedArgumentsQueue = [];
- control.expanded.bind( function ( expanded ) {
+ control.expanded.bind( function( expanded ) {
var args = control.expandedArgumentsQueue.shift();
args = $.extend( {}, control.defaultExpandedArguments, args );
control.onChangeExpanded( expanded, args );
});
- control.expanded.set( false );
+
+ api.Control.prototype.initialize.call( control, id, options );
},
/**
- * Set up the control
+ * Set up the control.
+ *
+ * @since 3.9.0
*/
ready: function() {
- this._setupModel();
- this._setupWideWidget();
- this._setupControlToggle();
- this._setupWidgetTitle();
- this._setupReorderUI();
- this._setupHighlightEffects();
- this._setupUpdateUI();
- this._setupRemoveUI();
+ var control = this;
+
+ /*
+ * Embed a placeholder once the section is expanded. The full widget
+ * form content will be embedded once the control itself is expanded,
+ * and at this point the widget-added event will be triggered.
+ */
+ if ( ! control.section() ) {
+ control.embedWidgetControl();
+ } else {
+ api.section( control.section(), function( section ) {
+ var onExpanded = function( isExpanded ) {
+ if ( isExpanded ) {
+ control.embedWidgetControl();
+ section.expanded.unbind( onExpanded );
+ }
+ };
+ if ( section.expanded() ) {
+ onExpanded( true );
+ } else {
+ section.expanded.bind( onExpanded );
+ }
+ } );
+ }
+ },
+
+ /**
+ * Embed the .widget element inside the li container.
+ *
+ * @since 4.4.0
+ */
+ embedWidgetControl: function() {
+ var control = this, widgetControl;
+
+ if ( control.widgetControlEmbedded ) {
+ return;
+ }
+ control.widgetControlEmbedded = true;
+
+ widgetControl = $( control.params.widget_control );
+ control.container.append( widgetControl );
+
+ control._setupModel();
+ control._setupWideWidget();
+ control._setupControlToggle();
+
+ control._setupWidgetTitle();
+ control._setupReorderUI();
+ control._setupHighlightEffects();
+ control._setupUpdateUI();
+ control._setupRemoveUI();
+ },
+
+ /**
+ * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
+ *
+ * @since 4.4.0
+ */
+ embedWidgetContent: function() {
+ var control = this, widgetContent;
+
+ control.embedWidgetControl();
+ if ( control.widgetContentEmbedded ) {
+ return;
+ }
+ control.widgetContentEmbedded = true;
+
+ widgetContent = $( control.params.widget_content );
+ control.container.find( '.widget-content:first' ).append( widgetContent );
/*
* Trigger widget-added event so that plugins can attach any event
* listeners and dynamic UI elements.
*/
- $( document ).trigger( 'widget-added', [ this.container.find( '.widget:first' ) ] );
+ $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
+
},
/**
_setupModel: function() {
var self = this, rememberSavedWidgetId;
- api.Widgets.savedWidgetIds = api.Widgets.savedWidgetIds || [];
-
// Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
rememberSavedWidgetId = function() {
api.Widgets.savedWidgetIds[self.params.widget_id] = true;
var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
+ // The updateWidget logic requires that the form fields to be fully present.
+ self.embedWidgetContent();
+
args = $.extend( {
instance: null,
complete: null,
onChangeExpanded: function ( expanded, args ) {
var self = this, $widget, $inside, complete, prevComplete;
+ self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
+ if ( expanded ) {
+ self.embedWidgetContent();
+ }
+
// If the expanded state is unchanged only manipulate container expanded states
if ( args.unchanged ) {
if ( expanded ) {
}
} );
+ /**
+ * 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 = $( '<div></div>', {
+ 'class': 'no-widget-areas-rendered-notice'
+ });
+ noRenderedAreasNotice.append( $( '<em></em>', {
+ 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
*
/**
* Keyboard-accessible reordering
*/
- this.container.find( '.reorder-toggle' ).on( 'click keydown', function( event ) {
- if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
- return;
- }
-
+ this.container.find( '.reorder-toggle' ).on( 'click', function() {
self.toggleReordering( ! self.isReordering );
} );
},
_setupAddition: function() {
var self = this;
- this.container.find( '.add-new-widget' ).on( 'click keydown', function( event ) {
- if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
- return;
- }
+ this.container.find( '.add-new-widget' ).on( 'click', function() {
+ var addNewWidgetBtn = $( this );
if ( self.$sectionContent.hasClass( 'reordering' ) ) {
return;
}
if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
+ addNewWidgetBtn.attr( 'aria-expanded', 'true' );
api.Widgets.availableWidgetsPanel.open( self );
} else {
+ addNewWidgetBtn.attr( 'aria-expanded', 'false' );
api.Widgets.availableWidgetsPanel.close();
}
} );
* @todo We should have a reordering state instead and rename this to onChangeReordering
*/
toggleReordering: function( showOrHide ) {
+ var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
+ reorderBtn = this.container.find( '.reorder-toggle' ),
+ widgetsTitle = this.$sectionContent.find( '.widget-title' );
+
showOrHide = Boolean( showOrHide );
if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
formControl.collapse();
} );
- this.$sectionContent.find( '.first-widget .move-widget' ).focus();
- this.$sectionContent.find( '.add-new-widget' ).prop( 'tabIndex', -1 );
+ addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
+ reorderBtn.attr( 'aria-label', l10n.reorderLabelOff );
+ wp.a11y.speak( l10n.reorderModeOn );
+ // Hide widget titles while reordering: title is already in the reorder controls.
+ widgetsTitle.attr( 'aria-hidden', 'true' );
} else {
- this.$sectionContent.find( '.add-new-widget' ).prop( 'tabIndex', 0 );
+ addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
+ reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
+ wp.a11y.speak( l10n.reorderModeOff );
+ widgetsTitle.attr( 'aria-hidden', 'false' );
}
},
is_new: ! isExistingWidget,
width: widget.get( 'width' ),
height: widget.get( 'height' ),
- is_wide: widget.get( 'is_wide' )
+ is_wide: widget.get( 'is_wide' ),
+ active: true
},
previewer: self.setting.previewer
} );
}
} );
- // 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
});