]> scripts.mit.edu Git - autoinstalls/wordpress.git/blobdiff - wp-admin/js/customize-widgets.js
WordPress 4.4
[autoinstalls/wordpress.git] / wp-admin / js / customize-widgets.js
index 47505dd34c98d385a0f3d7e7ea056e94d3cb8657..820e8e3cab6abac50f780651a4023bdecbd04011 100644 (file)
@@ -5,9 +5,10 @@
 
        // Set up our namespace...
        var api = wp.customize,
-               l10n, OldPreviewer;
+               l10n;
 
        api.Widgets = api.Widgets || {};
+       api.Widgets.savedWidgetIds = {};
 
        // Link settings
        api.Widgets.data = _wpCustomizeWidgetsSettings || {};
 
                        // If the available widgets panel is open and the customize controls are
                        // interacted with (i.e. available widgets panel is blurred) then close the
-                       // available widgets panel.
-                       $( '#customize-controls' ).on( 'click keydown', function( e ) {
+                       // available widgets panel. Also close on back button click.
+                       $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
                                var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
                                if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
                                        self.close();
                        } );
 
                        // Close the panel if the URL in the preview changes
-                       api.Widgets.Previewer.bind( 'url', this.close );
+                       api.previewer.bind( 'url', this.close );
                },
 
                // Performs a search and handles selected widget
 
                // Adds a selected widget to the sidebar
                submit: function( widgetTpl ) {
-                       var widgetId, widget;
+                       var widgetId, widget, widgetFormControl;
 
                        if ( ! widgetTpl ) {
                                widgetTpl = this.selected;
                                return;
                        }
 
-                       this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
+                       widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
+                       if ( widgetFormControl ) {
+                               widgetFormControl.focus();
+                       }
 
                        this.close();
                },
                        // Reset search
                        this.collection.doSearch( '' );
 
-                       this.$search.focus();
+                       if ( ! api.settings.browser.mobile ) {
+                               this.$search.focus();
+                       }
                },
 
                // Closes the panel
                                isEsc = ( event.which === 27 ),
                                isDown = ( event.which === 40 ),
                                isUp = ( event.which === 38 ),
+                               isTab = ( event.which === 9 ),
+                               isShift = ( event.shiftKey ),
                                selected = null,
                                firstVisible = this.$el.find( '> .widget-tpl:visible:first' ),
                                lastVisible = this.$el.find( '> .widget-tpl:visible:last' ),
-                               isSearchFocused = $( event.target ).is( this.$search );
+                               isSearchFocused = $( event.target ).is( this.$search ),
+                               isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' );
 
                        if ( isDown || isUp ) {
                                if ( isDown ) {
                        } else if ( isEsc ) {
                                this.close( { returnFocus: true } );
                        }
+
+                       if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
+                               this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
+                               event.preventDefault();
+                       }
                }
        });
 
         * @augments wp.customize.Control
         */
        api.Widgets.WidgetControl = api.Control.extend({
+               defaultExpandedArguments: {
+                       duration: 'fast',
+                       completeCallback: $.noop
+               },
+
                /**
-                * Set up the control
+                * @since 4.1.0
+                */
+               initialize: function( id, options ) {
+                       var control = this;
+
+                       control.widgetControlEmbedded = false;
+                       control.widgetContentEmbedded = false;
+                       control.expanded = new api.Value( false );
+                       control.expandedArgumentsQueue = [];
+                       control.expanded.bind( function( expanded ) {
+                               var args = control.expandedArgumentsQueue.shift();
+                               args = $.extend( {}, control.defaultExpandedArguments, args );
+                               control.onChangeExpanded( expanded, args );
+                       });
+
+                       api.Control.prototype.initialize.call( control, id, options );
+               },
+
+               /**
+                * 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', [ 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;
                                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
                        } );
                },
                         * Update available sidebars when their rendered state changes
                         */
                        updateAvailableSidebars = function() {
-                               var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem;
+                               var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
+                                       renderedSidebarCount = 0;
 
                                selfSidebarItem = $sidebarItems.filter( function(){
                                        return $( this ).data( 'id' ) === self.params.sidebar_id;
 
                                $sidebarItems.each( function() {
                                        var li = $( this ),
-                                               sidebarId,
-                                               sidebar;
+                                               sidebarId, sidebar, sidebarIsRendered;
 
                                        sidebarId = li.data( 'id' );
                                        sidebar = api.Widgets.registeredSidebars.get( sidebarId );
+                                       sidebarIsRendered = sidebar.get( 'is_rendered' );
 
-                                       li.toggle( sidebar.get( 'is_rendered' ) );
+                                       li.toggle( sidebarIsRendered );
 
-                                       if ( li.hasClass( 'selected' ) && ! sidebar.get( 'is_rendered' ) ) {
+                                       if ( sidebarIsRendered ) {
+                                               renderedSidebarCount += 1;
+                                       }
+
+                                       if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
                                                selectSidebarItem( selfSidebarItem );
                                        }
                                } );
+
+                               if ( renderedSidebarCount > 1 ) {
+                                       self.container.find( '.move-widget' ).show();
+                               } else {
+                                       self.container.find( '.move-widget' ).hide();
+                               }
                        };
 
                        updateAvailableSidebars();
                         * Handle clicks for up/down/move on the reorder nav
                         */
                        $reorderNav = this.container.find( '.widget-reorder-nav' );
-                       $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).on( 'click keypress', function( event ) {
+                       $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
+                               $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
+                       } ).on( 'click keypress', function( event ) {
                                if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
                                        return;
                                }
 
                                        if ( isMoveUp ) {
                                                self.moveUp();
+                                               wp.a11y.speak( l10n.widgetMovedUp );
                                        } else {
                                                self.moveDown();
+                                               wp.a11y.speak( l10n.widgetMovedDown );
                                        }
 
                                        $( this ).focus(); // re-focus after the container was moved
                        /**
                         * Handle selecting a sidebar to move to
                         */
-                       this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( e ) {
+                       this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
                                if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
                                        return;
                                }
-                               e.preventDefault();
+                               event.preventDefault();
                                selectSidebarItem( $( this ) );
                        } );
 
                },
 
                /**
-                * Highlight widgets in preview when interacted with in the customizer
+                * Highlight widgets in preview when interacted with in the Customizer
                 */
                _setupHighlightEffects: function() {
                        var self = this;
 
                        // Handle widgets that support live previews
                        $widgetContent.on( 'change input propertychange', ':input', function( e ) {
-                               if ( self.liveUpdateMode ) {
-                                       if ( e.type === 'change' ) {
-                                               self.updateWidget();
-                                       } else if ( this.checkValidity && this.checkValidity() ) {
-                                               updateWidgetDebounced();
-                                       }
+                               if ( ! self.liveUpdateMode ) {
+                                       return;
+                               }
+                               if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
+                                       updateWidgetDebounced();
                                }
                        } );
 
                                self.container.removeClass( 'previewer-loading' );
                        } );
 
-                       api.Widgets.Previewer.bind( 'widget-updated', function( updatedWidgetId ) {
+                       api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
                                if ( updatedWidgetId === self.params.widget_id ) {
                                        self.container.removeClass( 'previewer-loading' );
                                }
                        } );
 
-                       // Update widget control to indicate whether it is currently rendered
-                       api.Widgets.Previewer.bind( 'rendered-widgets', function( renderedWidgets ) {
-                               var isRendered = !! renderedWidgets[self.params.widget_id];
-
-                               self.container.toggleClass( 'widget-rendered', isRendered );
-                       } );
-
                        formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
                        if ( formSyncHandler ) {
                                $( document ).on( 'widget-synced', function( e, widget ) {
                        }
                },
 
+               /**
+                * Update widget control to indicate whether it is currently rendered.
+                *
+                * Overrides api.Control.toggle()
+                *
+                * @since 4.1.0
+                *
+                * @param {Boolean}   active
+                * @param {Object}    args
+                * @param {Callback}  args.completeCallback
+                */
+               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();
+                       }
+               },
+
                /**
                 * Set up event handlers for widget removal
                 */
                },
 
                /**
-                * Get the property that represents the state of an input.
+                * Get the state for an input depending on its type.
                 *
-                * @param {jQuery|DOMElement} input
-                * @returns {string}
+                * @param {jQuery|Element} input
+                * @returns {string|boolean|array|*}
                 * @private
                 */
-               _getInputStatePropertyName: function( input ) {
-                       var $input = $( input );
+               _getInputState: function( input ) {
+                       input = $( input );
+                       if ( input.is( ':radio, :checkbox' ) ) {
+                               return input.prop( 'checked' );
+                       } else if ( input.is( 'select[multiple]' ) ) {
+                               return input.find( 'option:selected' ).map( function () {
+                                       return $( this ).val();
+                               } ).get();
+                       } else {
+                               return input.val();
+                       }
+               },
 
-                       if ( $input.is( ':radio, :checkbox' ) ) {
-                               return 'checked';
+               /**
+                * Update an input's state based on its type.
+                *
+                * @param {jQuery|Element} input
+                * @param {string|boolean|array|*} state
+                * @private
+                */
+               _setInputState: function ( input, state ) {
+                       input = $( input );
+                       if ( input.is( ':radio, :checkbox' ) ) {
+                               input.prop( 'checked', state );
+                       } else if ( input.is( 'select[multiple]' ) ) {
+                               if ( ! $.isArray( state ) ) {
+                                       state = [];
+                               } else {
+                                       // Make sure all state items are strings since the DOM value is a string
+                                       state = _.map( state, function ( value ) {
+                                               return String( value );
+                                       } );
+                               }
+                               input.find( 'option' ).each( function () {
+                                       $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) );
+                               } );
                        } else {
-                               return 'value';
+                               input.val( state );
                        }
                },
 
                        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,
                        params.wp_customize = 'on';
                        params.nonce = api.Widgets.data.nonce;
                        params.theme = api.settings.theme.stylesheet;
+                       params.customized = wp.customize.previewer.query().customized;
 
                        data = $.param( params );
                        $inputs = this._getInputs( $widgetContent );
                        // we know if it got sanitized; if there is no difference in the sanitized value,
                        // then we do not need to touch the UI and mess up the user's ongoing editing.
                        $inputs.each( function() {
-                               var input = $( this ),
-                                       property = self._getInputStatePropertyName( this );
-                               input.data( 'state' + updateNumber, input.prop( property ) );
+                               $( this ).data( 'state' + updateNumber, self._getInputState( this ) );
                        } );
 
                        if ( instanceOverride ) {
                        }
                        data += '&' + $widgetContent.find( '~ :input' ).serialize();
 
+                       if ( this._previousUpdateRequest ) {
+                               this._previousUpdateRequest.abort();
+                       }
                        jqxhr = $.post( wp.ajax.settings.url, data );
+                       this._previousUpdateRequest = jqxhr;
 
                        jqxhr.done( function( r ) {
                                var message, sanitizedForm,     $sanitizedInputs, hasSameInputsInResponse,
 
                                // Check if the user is logged out.
                                if ( '0' === r ) {
-                                       api.Widgets.Previewer.preview.iframe.hide();
-                                       api.Widgets.Previewer.login().done( function() {
+                                       api.previewer.preview.iframe.hide();
+                                       api.previewer.login().done( function() {
                                                self.updateWidget( args );
-                                               api.Widgets.Previewer.preview.iframe.show();
+                                               api.previewer.preview.iframe.show();
                                        } );
                                        return;
                                }
 
                                // Check for cheaters.
                                if ( '-1' === r ) {
-                                       api.Widgets.Previewer.cheatin();
+                                       api.previewer.cheatin();
                                        return;
                                }
 
                                                $inputs.each( function( i ) {
                                                        var $input = $( this ),
                                                                $sanitizedInput = $( $sanitizedInputs[i] ),
-                                                               property = self._getInputStatePropertyName( this ),
                                                                submittedState, sanitizedState, canUpdateState;
 
                                                        submittedState = $input.data( 'state' + updateNumber );
-                                                       sanitizedState = $sanitizedInput.prop( property );
+                                                       sanitizedState = self._getInputState( $sanitizedInput );
                                                        $input.data( 'sanitized', sanitizedState );
 
-                                                       canUpdateState = ( submittedState !== sanitizedState && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) )   );
+                                                       canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) );
                                                        if ( canUpdateState ) {
-                                                               $input.prop( property, sanitizedState );
+                                                               self._setInputState( $input, sanitizedState );
                                                        }
                                                } );
 
                 * 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 );
                },
 
+               /**
+                * @since 4.1.0
+                *
+                * @param {Boolean} expanded
+                * @param {Object} [params]
+                * @returns {Boolean} false if state already applied
+                */
+               _toggleExpanded: api.Section.prototype._toggleExpanded,
+
+               /**
+                * @since 4.1.0
+                *
+                * @param {Object} [params]
+                * @returns {Boolean} false if already expanded
+                */
+               expand: api.Section.prototype.expand,
+
                /**
                 * Expand the widget form control
+                *
+                * @deprecated 4.1.0 Use this.expand() instead.
                 */
                expandForm: function() {
-                       this.toggleForm( true );
+                       this.expand();
                },
 
+               /**
+                * @since 4.1.0
+                *
+                * @param {Object} [params]
+                * @returns {Boolean} false if already collapsed
+                */
+               collapse: api.Section.prototype.collapse,
+
                /**
                 * Collapse the widget form control
+                *
+                * @deprecated 4.1.0 Use this.collapse() instead.
                 */
                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 );
+               },
+
+               /**
+                * 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;
+
+                       self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
+                       if ( expanded ) {
+                               self.embedWidgetContent();
                        }
 
-                       // Already expanded or collapsed, so noop
-                       if ( $inside.is( ':visible' ) === showOrHide ) {
+                       // 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 ) {
+
+                               if ( self.section() && api.section( self.section() ) ) {
+                                       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();
                                        }
                                } );
 
                                        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();
                                        } );
                        }
                },
 
-               /**
-                * 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
                 *
                }
        } );
 
+       /**
+        * 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
+        *
+        * Customizer section representing a widget area widget
+        *
+        * @since 4.1.0
+        */
+       api.Widgets.SidebarSection = api.Section.extend({
+
+               /**
+                * Sync the section's active state back to the Backbone model's is_rendered attribute
+                *
+                * @since 4.1.0
+                */
+               ready: function () {
+                       var section = this, registeredSidebar;
+                       api.Section.prototype.ready.call( this );
+                       registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId );
+                       section.active.bind( function ( active ) {
+                               registeredSidebar.set( 'is_rendered', active );
+                       });
+                       registeredSidebar.set( 'is_rendered', section.active() );
+               }
+       });
+
        /**
         * wp.customize.Widgets.SidebarControl
         *
         * Customizer control for widgets.
         * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type
         *
+        * @since 3.9.0
+        *
         * @constructor
         * @augments wp.customize.Control
         */
        api.Widgets.SidebarControl = api.Control.extend({
+
                /**
                 * Set up the control
                 */
                 * Update ordering of widget control forms when the setting is updated
                 */
                _setupModel: function() {
-                       var self = this,
-                               registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id );
+                       var self = this;
 
                        this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
-                               var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;
+                               var widgetFormControls, removedWidgetIds, priority;
 
                                removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
 
                                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();
 
                                } );
                        } );
-
-                       // Update the model with whether or not the sidebar is rendered
-                       api.Widgets.Previewer.bind( 'rendered-sidebars', function( renderedSidebars ) {
-                               var isRendered = !! renderedSidebars[self.params.sidebar_id];
-
-                               registeredSidebar.set( 'is_rendered', isRendered );
-                       } );
-
-                       // Show the sidebar section when it becomes visible
-                       registeredSidebar.on( 'change:is_rendered', function( ) {
-                               var sectionSelector = '#accordion-section-sidebar-widgets-' + this.get( 'id' ), $section;
-
-                               $section = $( sectionSelector );
-                               if ( this.get( 'is_rendered' ) ) {
-                                       $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();
-                               }
-                       } );
                },
 
                /**
                                items: '> .customize-control-widget_form',
                                handle: '.widget-top',
                                axis: 'y',
+                               tolerance: 'pointer',
                                connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
                                update: function() {
                                        var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
                        } );
 
                        /**
-                        * Expand other customizer sidebar section when dragging a control widget over it,
+                        * Expand other Customizer sidebar section when dragging a control widget over it,
                         * allowing the control to be dropped into another section
                         */
                        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' );
+                                                               }
+                                                       } );
+                                               }
+                                       });
                                }
                        });
 
                        /**
                         * 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();
                                }
                        } );
                 * 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 );
+                               }
+                       });
+
+                       if ( ! widgetControls.length ) {
+                               this.container.find( '.reorder-toggle' ).hide();
+                               return;
+                       } else {
+                               this.container.find( '.reorder-toggle' ).show();
+                       }
 
-                       this.$sectionContent.find( '.customize-control-widget_form:first' )
+                       $( 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 );
                },
                 * 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 ) {
+                       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' ) ) {
 
                        if ( showOrHide ) {
                                _( this.getWidgetFormControls() ).each( function( formControl ) {
-                                       formControl.collapseForm();
+                                       formControl.collapse();
                                } );
+
+                               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 {
+                               addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
+                               reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
+                               wp.a11y.speak( l10n.reorderModeOff );
+                               widgetsTitle.attr( 'aria-hidden', 'false' );
                        }
                },
 
                /**
+                * Get the widget_form Customize controls associated with the current sidebar.
+                *
+                * @since 3.9
                 * @return {wp.customize.controlConstructor.widget_form[]}
                 */
                getWidgetFormControls: function() {
-                       var formControls;
+                       var formControls = [];
 
-                       formControls = _( this.setting() ).map( function( widgetId ) {
+                       _( this.setting() ).each( function( widgetId ) {
                                var settingId = widgetIdToSettingId( widgetId ),
                                        formControl = api.control( settingId );
-
-                               if ( ! formControl ) {
-                                       return;
+                               if ( formControl ) {
+                                       formControls.push( formControl );
                                }
-
-                               return formControl;
                        } );
 
                        return formControls;
                 * @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,
                                widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ),
-                               settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs;
+                               settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting;
 
                        if ( ! widget ) {
                                return false;
 
                        $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 );
                                        transport: 'refresh',
                                        previewer: this.setting.previewer
                                };
-                               api.create( settingId, settingId, {}, settingArgs );
+                               setting = api.create( settingId, settingId, '', settingArgs );
+                               setting.set( {} ); // mark dirty, changing from '' to {}
                        }
 
                        controlConstructor = api.controlConstructor[controlType];
                                        settings: {
                                                'default': settingId
                                        },
+                                       content: controlContainer,
                                        sidebar_id: self.params.sidebar_id,
                                        widget_id: widgetId,
                                        widget_id_base: widget.get( 'id_base' ),
                                        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
                        } );
                                this.setting( sidebarWidgets );
                        }
 
-                       $control.slideDown( function() {
+                       controlContainer.slideDown( function() {
                                if ( isExistingWidget ) {
-                                       widgetFormControl.expandForm();
                                        widgetFormControl.updateWidget( {
-                                               instance: widgetFormControl.setting(),
-                                               complete: function( error ) {
-                                                       if ( error ) {
-                                                               throw error;
-                                                       }
-                                                       widgetFormControl.focus();
-                                               }
+                                               instance: widgetFormControl.setting()
                                        } );
-                               } else {
-                                       widgetFormControl.focus();
                                }
                        } );
 
-                       $( document ).trigger( 'widget-added', [ $widget ] );
-
                        return widgetFormControl;
                }
        } );
 
-       /**
-        * Extends wp.customizer.controlConstructor with control constructor for
-        * widget_form and sidebar_widgets.
-        */
+       // Register models for custom panel, section, and control types
+       $.extend( api.panelConstructor, {
+               widgets: api.Widgets.WidgetsPanel
+       });
+       $.extend( api.sectionConstructor, {
+               sidebar: api.Widgets.SidebarSection
+       });
        $.extend( api.controlConstructor, {
                widget_form: api.Widgets.WidgetControl,
                sidebar_widgets: api.Widgets.SidebarControl
        });
 
-       /**
-        * Capture the instance of the Previewer since it is private
-        */
-       OldPreviewer = api.Previewer;
-       api.Previewer = OldPreviewer.extend({
-               initialize: function( params, options ) {
-                       api.Widgets.Previewer = this;
-                       OldPreviewer.prototype.initialize.call( this, params, options );
-                       this.bind( 'refresh', this.refresh );
-               }
-       } );
+       // Refresh the nonce if login sends updated nonces over.
+       api.bind( 'nonce-refresh', function( nonces ) {
+               api.Widgets.data.nonce = nonces['update-widget'];
+       });
 
        /**
         * Init Customizer for widgets.
                });
 
                // Highlight widget control
-               api.Widgets.Previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
+               api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
 
                // Open and focus widget control
-               api.Widgets.Previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
+               api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
        } );
 
        /**