wp/wp-admin/js/customize-widgets.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
--- a/wp/wp-admin/js/customize-widgets.js	Tue Jun 09 11:14:17 2015 +0000
+++ b/wp/wp-admin/js/customize-widgets.js	Mon Oct 14 17:39:30 2019 +0200
@@ -8,11 +8,11 @@
 		l10n;
 
 	api.Widgets = api.Widgets || {};
+	api.Widgets.savedWidgetIds = {};
 
 	// Link settings
 	api.Widgets.data = _wpCustomizeWidgetsSettings || {};
 	l10n = api.Widgets.data.l10n;
-	delete api.Widgets.data.l10n;
 
 	/**
 	 * wp.customize.Widgets.WidgetModel
@@ -33,7 +33,7 @@
 		multi_number: null,
 		name: null,
 		id_base: null,
-		transport: 'refresh',
+		transport: null,
 		params: [],
 		width: null,
 		height: null,
@@ -69,8 +69,7 @@
 				this.search( this.terms );
 			}
 
-			// If search is blank, show all themes
-			// Useful for resetting the views when you clean the input
+			// If search is blank, set all the widgets as they matched the search to reset the views.
 			if ( this.terms === '' ) {
 				this.each( function ( widget ) {
 					widget.set( 'search_matched', true );
@@ -148,8 +147,6 @@
 		events: {
 			'input #widgets-search': 'search',
 			'keyup #widgets-search': 'search',
-			'change #widgets-search': 'search',
-			'search #widgets-search': 'search',
 			'focus .widget-tpl' : 'focus',
 			'click .widget-tpl' : '_submit',
 			'keypress .widget-tpl' : '_submit',
@@ -162,28 +159,40 @@
 		// Cache sidebar control which has opened panel
 		currentSidebarControl: null,
 		$search: null,
+		$clearResults: null,
+		searchMatchesCount: null,
 
 		initialize: function() {
 			var self = this;
 
 			this.$search = $( '#widgets-search' );
 
+			this.$clearResults = this.$el.find( '.clear-results' );
+
 			_.bindAll( this, 'close' );
 
 			this.listenTo( this.collection, 'change', this.updateList );
 
 			this.updateList();
 
+			// Set the initial search count to the number of available widgets.
+			this.searchMatchesCount = this.collection.length;
+
 			// 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, .customize-overlay-close' ).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();
 				}
 			} );
 
+			// Clear the search results and trigger a `keyup` event to fire a new search.
+			this.$clearResults.on( 'click', function() {
+				self.$search.val( '' ).focus().trigger( 'keyup' );
+			} );
+
 			// Close the panel if the URL in the preview changes
 			api.previewer.bind( 'url', this.close );
 		},
@@ -193,6 +202,10 @@
 			var firstVisible;
 
 			this.collection.doSearch( event.target.value );
+			// Update the search matches count.
+			this.updateSearchMatchesCount();
+			// Announce how many search results.
+			this.announceSearchMatches();
 
 			// Remove a widget from being selected if it is no longer visible
 			if ( this.selected && ! this.selected.is( ':visible' ) ) {
@@ -213,8 +226,38 @@
 					this.select( firstVisible );
 				}
 			}
+
+			// Toggle the clear search results button.
+			if ( '' !== event.target.value ) {
+				this.$clearResults.addClass( 'is-visible' );
+			} else if ( '' === event.target.value ) {
+				this.$clearResults.removeClass( 'is-visible' );
+			}
+
+			// Set a CSS class on the search container when there are no search results.
+			if ( ! this.searchMatchesCount ) {
+				this.$el.addClass( 'no-widgets-found' );
+			} else {
+				this.$el.removeClass( 'no-widgets-found' );
+			}
 		},
 
+		// Update the count of the available widgets that have the `search_matched` attribute.
+		updateSearchMatchesCount: function() {
+			this.searchMatchesCount = this.collection.where({ search_matched: true }).length;
+		},
+
+		// Send a message to the aria-live region to announce how many search results.
+		announceSearchMatches: _.debounce( function() {
+			var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ;
+
+			if ( ! this.searchMatchesCount ) {
+				message = l10n.noWidgetsFound;
+			}
+
+			wp.a11y.speak( message );
+		}, 500 ),
+
 		// Changes visibility of available widgets
 		updateList: function() {
 			this.collection.each( function( widget ) {
@@ -287,6 +330,10 @@
 				}
 			} );
 
+			if ( api.section.has( 'publish_settings' ) ) {
+				api.section( 'publish_settings' ).collapse();
+			}
+
 			$( 'body' ).addClass( 'adding-widget' );
 
 			this.$el.find( '.selected' ).removeClass( 'selected' );
@@ -366,7 +413,7 @@
 				this.close( { returnFocus: true } );
 			}
 
-			if ( isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
+			if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
 				this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
 				event.preventDefault();
 			}
@@ -417,37 +464,109 @@
 		/**
 		 * @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 );
+			control.altNotice = true;
+
+			api.Control.prototype.initialize.call( control, id, options );
+		},
+
+		/**
+		 * Set up the control.
+		 *
+		 * @since 3.9.0
+		 */
+		ready: function() {
+			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 );
+					}
+				} );
+			}
 		},
 
 		/**
-		 * Set up the control
+		 * Embed the .widget element inside the li container.
+		 *
+		 * @since 4.4.0
 		 */
-		ready: function() {
-			this._setupModel();
-			this._setupWideWidget();
-			this._setupControlToggle();
-			this._setupWidgetTitle();
-			this._setupReorderUI();
-			this._setupHighlightEffects();
-			this._setupUpdateUI();
-			this._setupRemoveUI();
+		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;
+
+			// Update the notification container element now that the widget content has been embedded.
+			control.notifications.container = control.getNotificationsContainerElement();
+			control.notifications.render();
+
+			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' ) ] );
+
 		},
 
 		/**
@@ -456,8 +575,6 @@
 		_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;
@@ -484,7 +601,7 @@
 			var self = this, $widgetInside, $widgetForm, $customizeSidebar,
 				$themeControlsContainer, positionWidget;
 
-			if ( ! this.params.is_wide ) {
+			if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) {
 				return;
 			}
 
@@ -493,7 +610,7 @@
 			$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
 			this.container.addClass( 'wide-widget-control' );
 
-			this.container.find( '.widget-content:first' ).css( {
+			this.container.find( '.form:first' ).css( {
 				'max-width': this.params.width,
 				'min-height': this.params.height
 			} );
@@ -597,7 +714,7 @@
 		 */
 		_setupReorderUI: function() {
 			var self = this, selectSidebarItem, $moveWidgetArea,
-				$reorderNav, updateAvailableSidebars;
+				$reorderNav, updateAvailableSidebars, template;
 
 			/**
 			 * select the provided sidebar list item in the move widget area
@@ -615,8 +732,10 @@
 			 * Add the widget reordering elements to the widget control
 			 */
 			this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
-			$moveWidgetArea = $(
-				_.template( api.Widgets.data.tpl.moveWidgetArea, {
+
+
+			template = _.template( api.Widgets.data.tpl.moveWidgetArea );
+			$moveWidgetArea = $( template( {
 					sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
 				} )
 			);
@@ -766,7 +885,7 @@
 			$saveBtn = this.container.find( '.widget-control-save' );
 			$saveBtn.val( l10n.saveBtnLabel );
 			$saveBtn.attr( 'title', l10n.saveBtnTooltip );
-			$saveBtn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
+			$saveBtn.removeClass( 'button-primary' );
 			$saveBtn.on( 'click', function( e ) {
 				e.preventDefault();
 				self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
@@ -786,12 +905,11 @@
 
 			// 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();
 				}
 			} );
 
@@ -842,7 +960,7 @@
 			var self = this, $removeBtn, replaceDeleteWithRemove;
 
 			// Configure remove button
-			$removeBtn = this.container.find( 'a.widget-control-remove' );
+			$removeBtn = this.container.find( '.widget-control-remove' );
 			$removeBtn.on( 'click', function( e ) {
 				e.preventDefault();
 
@@ -878,7 +996,7 @@
 			} );
 
 			replaceDeleteWithRemove = function() {
-				$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete"
+				$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete"
 				$removeBtn.attr( 'title', l10n.removeBtnTooltip );
 			};
 
@@ -894,7 +1012,7 @@
 		 * comparing the loaded form with the sanitized form, whose fields will
 		 * be aligned to copy the sanitized over. The elements returned by this
 		 * are passed into this._getInputsSignature(), and they are iterated
-		 * over when copying sanitized values over to the the form loaded.
+		 * over when copying sanitized values over to the form loaded.
 		 *
 		 * @param {jQuery} container element in which to look for inputs
 		 * @returns {jQuery} inputs
@@ -1009,6 +1127,9 @@
 			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,
@@ -1039,8 +1160,9 @@
 			params = {};
 			params.action = 'update-widget';
 			params.wp_customize = 'on';
-			params.nonce = api.Widgets.data.nonce;
-			params.theme = api.settings.theme.stylesheet;
+			params.nonce = api.settings.nonce['update-widget'];
+			params.customize_theme = api.settings.theme.stylesheet;
+			params.customized = wp.customize.previewer.query().customized;
 
 			data = $.param( params );
 			$inputs = this._getInputs( $widgetContent );
@@ -1253,7 +1375,12 @@
 		 * @param {Object} args  merged on top of this.defaultActiveArguments
 		 */
 		onChangeExpanded: function ( expanded, args ) {
-			var self = this, $widget, $inside, complete, prevComplete;
+			var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
+
+			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 ) {
@@ -1267,10 +1394,9 @@
 
 			$widget = this.container.find( 'div.widget:first' );
 			$inside = $widget.find( '.widget-inside:first' );
+			$toggleBtn = this.container.find( '.widget-top button.widget-action' );
 
-			if ( expanded ) {
-
-				self.expandControlSection();
+			expandControl = function() {
 
 				// Close all other widget controls before expanding this one
 				api.control.each( function( otherControl ) {
@@ -1282,6 +1408,8 @@
 				complete = function() {
 					self.container.removeClass( 'expanding' );
 					self.container.addClass( 'expanded' );
+					$widget.addClass( 'open' );
+					$toggleBtn.attr( 'aria-expanded', 'true' );
 					self.container.trigger( 'expanded' );
 				};
 				if ( args.completeCallback ) {
@@ -1300,11 +1428,22 @@
 
 				self.container.trigger( 'expand' );
 				self.container.addClass( 'expanding' );
+			};
+
+			if ( expanded ) {
+				if ( api.section.has( self.section() ) ) {
+					api.section( self.section() ).expand( {
+						completeCallback: expandControl
+					} );
+				} else {
+					expandControl();
+				}
 			} else {
-
 				complete = function() {
 					self.container.removeClass( 'collapsing' );
 					self.container.removeClass( 'expanded' );
+					$widget.removeClass( 'open' );
+					$toggleBtn.attr( 'aria-expanded', 'false' );
 					self.container.trigger( 'collapsed' );
 				};
 				if ( args.completeCallback ) {
@@ -1430,6 +1569,127 @@
 	} );
 
 	/**
+	 * 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, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice;
+				panelMetaContainer = panel.container.find( '.panel-meta' );
+
+				// @todo This should use the Notifications API introduced to panels. See <https://core.trac.wordpress.org/ticket/38794>.
+				noticeContainer = $( '<div></div>', {
+					'class': 'no-widget-areas-rendered-notice'
+				});
+				panelMetaContainer.append( noticeContainer );
+
+				/**
+				 * Get the number of active sections in the panel.
+				 *
+				 * @return {number} Number of active sidebar sections.
+				 */
+				getActiveSectionCount = function() {
+					return _.filter( panel.sections(), function( section ) {
+						return section.active();
+					} ).length;
+				};
+
+				/**
+				 * Determine whether or not the notice should be displayed.
+				 *
+				 * @return {boolean}
+				 */
+				shouldShowNotice = function() {
+					var activeSectionCount = getActiveSectionCount();
+					if ( 0 === activeSectionCount ) {
+						return true;
+					} else {
+						return activeSectionCount !== api.Widgets.data.registeredSidebars.length;
+					}
+				};
+
+				/**
+				 * Update the notice.
+				 *
+				 * @returns {void}
+				 */
+				updateNotice = function() {
+					var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
+					noticeContainer.empty();
+
+					registeredAreaCount = api.Widgets.data.registeredSidebars.length;
+					if ( activeSectionCount !== registeredAreaCount ) {
+
+						if ( 0 !== activeSectionCount ) {
+							nonRenderedAreaCount = registeredAreaCount - activeSectionCount;
+							someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ];
+						} else {
+							someRenderedMessage = l10n.noAreasShown;
+						}
+						if ( someRenderedMessage ) {
+							noticeContainer.append( $( '<p></p>', {
+								text: someRenderedMessage
+							} ) );
+						}
+
+						noticeContainer.append( $( '<p></p>', {
+							text: l10n.navigatePreview
+						} ) );
+					}
+				};
+				updateNotice();
+
+				/*
+				 * Set the initial visibility state for rendered notice.
+				 * Update the visibility of the notice whenever a reflow happens.
+				 */
+				noticeContainer.toggle( shouldShowNotice() );
+				api.previewer.deferred.active.done( function () {
+					noticeContainer.toggle( shouldShowNotice() );
+				});
+				api.bind( 'pane-contents-reflowed', function() {
+					var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0;
+					updateNotice();
+					if ( shouldShowNotice() ) {
+						noticeContainer.slideDown( duration );
+					} else {
+						noticeContainer.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
@@ -1604,6 +1864,7 @@
 				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;
@@ -1641,11 +1902,7 @@
 			/**
 			 * 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 );
 			} );
 		},
@@ -1656,18 +1913,18 @@
 		_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();
 				}
 			} );
@@ -1685,7 +1942,7 @@
 				}
 			});
 
-			if ( ! widgetControls.length ) {
+			if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) {
 				this.container.find( '.reorder-toggle' ).hide();
 				return;
 			} else {
@@ -1721,6 +1978,10 @@
 		 * @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' ) ) {
@@ -1735,17 +1996,23 @@
 					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' );
 			}
 		},
 
 		/**
 		 * Get the widget_form Customize controls associated with the current sidebar.
 		 *
-		 * @since 3.9
+		 * @since 3.9.0
 		 * @return {wp.customize.controlConstructor.widget_form[]}
 		 */
 		getWidgetFormControls: function() {
@@ -1826,7 +2093,7 @@
 			isExistingWidget = api.has( settingId );
 			if ( ! isExistingWidget ) {
 				settingArgs = {
-					transport: 'refresh',
+					transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
 					previewer: this.setting.previewer
 				};
 				setting = api.create( settingId, settingId, '', settingArgs );
@@ -1835,23 +2102,20 @@
 
 			controlConstructor = api.controlConstructor[controlType];
 			widgetFormControl = new controlConstructor( settingId, {
-				params: {
-					settings: {
-						'default': settingId
-					},
-					content: controlContainer,
-					sidebar_id: self.params.sidebar_id,
-					widget_id: widgetId,
-					widget_id_base: widget.get( 'id_base' ),
-					type: controlType,
-					is_new: ! isExistingWidget,
-					width: widget.get( 'width' ),
-					height: widget.get( 'height' ),
-					is_wide: widget.get( 'is_wide' )
+				settings: {
+					'default': settingId
 				},
-				previewer: self.setting.previewer
+				content: controlContainer,
+				sidebar_id: self.params.sidebar_id,
+				widget_id: widgetId,
+				widget_id_base: widget.get( 'id_base' ),
+				type: controlType,
+				is_new: ! isExistingWidget,
+				width: widget.get( 'width' ),
+				height: widget.get( 'height' ),
+				is_wide: widget.get( 'is_wide' )
 			} );
-			api.control.add( settingId, widgetFormControl );
+			api.control.add( widgetFormControl );
 
 			// Make sure widget is removed from the other sidebars
 			api.each( function( otherSetting ) {
@@ -1891,7 +2155,10 @@
 		}
 	} );
 
-	// Register models for custom section and control types
+	// Register models for custom panel, section, and control types
+	$.extend( api.panelConstructor, {
+		widgets: api.Widgets.WidgetsPanel
+	});
 	$.extend( api.sectionConstructor, {
 		sidebar: api.Widgets.SidebarSection
 	});
@@ -1900,11 +2167,6 @@
 		sidebar_widgets: api.Widgets.SidebarControl
 	});
 
-	// 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.
 	 */
@@ -1985,6 +2247,58 @@
 	};
 
 	/**
+	 * Initialize Edit Menu button in Nav Menu widget.
+	 */
+	$( document ).on( 'widget-added', function( event, widgetContainer ) {
+		var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton;
+		parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() );
+		if ( 'nav_menu' !== parsedWidgetId.id_base ) {
+			return;
+		}
+		widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' );
+		if ( ! widgetControl ) {
+			return;
+		}
+		navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' );
+		editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' );
+		if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) {
+			return;
+		}
+		navMenuSelect.on( 'change', function() {
+			if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) {
+				editMenuButton.parent().show();
+			} else {
+				editMenuButton.parent().hide();
+			}
+		});
+		editMenuButton.on( 'click', function() {
+			var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' );
+			if ( section ) {
+				focusConstructWithBreadcrumb( section, widgetControl );
+			}
+		} );
+	} );
+
+	/**
+	 * Focus (expand) one construct and then focus on another construct after the first is collapsed.
+	 *
+	 * This overrides the back button to serve the purpose of breadcrumb navigation.
+	 *
+	 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus.
+	 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus.
+	 */
+	function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) {
+		focusConstruct.focus();
+		function onceCollapsed( isExpanded ) {
+			if ( ! isExpanded ) {
+				focusConstruct.expanded.unbind( onceCollapsed );
+				returnConstruct.focus();
+			}
+		}
+		focusConstruct.expanded.bind( onceCollapsed );
+	}
+
+	/**
 	 * @param {String} widgetId
 	 * @returns {Object}
 	 */