wp/wp-admin/js/customize-widgets.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
     6 	// Set up our namespace...
     6 	// Set up our namespace...
     7 	var api = wp.customize,
     7 	var api = wp.customize,
     8 		l10n;
     8 		l10n;
     9 
     9 
    10 	api.Widgets = api.Widgets || {};
    10 	api.Widgets = api.Widgets || {};
       
    11 	api.Widgets.savedWidgetIds = {};
    11 
    12 
    12 	// Link settings
    13 	// Link settings
    13 	api.Widgets.data = _wpCustomizeWidgetsSettings || {};
    14 	api.Widgets.data = _wpCustomizeWidgetsSettings || {};
    14 	l10n = api.Widgets.data.l10n;
    15 	l10n = api.Widgets.data.l10n;
    15 	delete api.Widgets.data.l10n;
       
    16 
    16 
    17 	/**
    17 	/**
    18 	 * wp.customize.Widgets.WidgetModel
    18 	 * wp.customize.Widgets.WidgetModel
    19 	 *
    19 	 *
    20 	 * A single widget model.
    20 	 * A single widget model.
    31 		is_disabled: null,
    31 		is_disabled: null,
    32 		is_multi: null,
    32 		is_multi: null,
    33 		multi_number: null,
    33 		multi_number: null,
    34 		name: null,
    34 		name: null,
    35 		id_base: null,
    35 		id_base: null,
    36 		transport: 'refresh',
    36 		transport: null,
    37 		params: [],
    37 		params: [],
    38 		width: null,
    38 		width: null,
    39 		height: null,
    39 		height: null,
    40 		search_matched: true
    40 		search_matched: true
    41 	});
    41 	});
    67 			// If we have terms, run a search...
    67 			// If we have terms, run a search...
    68 			if ( this.terms.length > 0 ) {
    68 			if ( this.terms.length > 0 ) {
    69 				this.search( this.terms );
    69 				this.search( this.terms );
    70 			}
    70 			}
    71 
    71 
    72 			// If search is blank, show all themes
    72 			// If search is blank, set all the widgets as they matched the search to reset the views.
    73 			// Useful for resetting the views when you clean the input
       
    74 			if ( this.terms === '' ) {
    73 			if ( this.terms === '' ) {
    75 				this.each( function ( widget ) {
    74 				this.each( function ( widget ) {
    76 					widget.set( 'search_matched', true );
    75 					widget.set( 'search_matched', true );
    77 				} );
    76 				} );
    78 			}
    77 			}
   146 		el: '#available-widgets',
   145 		el: '#available-widgets',
   147 
   146 
   148 		events: {
   147 		events: {
   149 			'input #widgets-search': 'search',
   148 			'input #widgets-search': 'search',
   150 			'keyup #widgets-search': 'search',
   149 			'keyup #widgets-search': 'search',
   151 			'change #widgets-search': 'search',
       
   152 			'search #widgets-search': 'search',
       
   153 			'focus .widget-tpl' : 'focus',
   150 			'focus .widget-tpl' : 'focus',
   154 			'click .widget-tpl' : '_submit',
   151 			'click .widget-tpl' : '_submit',
   155 			'keypress .widget-tpl' : '_submit',
   152 			'keypress .widget-tpl' : '_submit',
   156 			'keydown' : 'keyboardAccessible'
   153 			'keydown' : 'keyboardAccessible'
   157 		},
   154 		},
   160 		selected: null,
   157 		selected: null,
   161 
   158 
   162 		// Cache sidebar control which has opened panel
   159 		// Cache sidebar control which has opened panel
   163 		currentSidebarControl: null,
   160 		currentSidebarControl: null,
   164 		$search: null,
   161 		$search: null,
       
   162 		$clearResults: null,
       
   163 		searchMatchesCount: null,
   165 
   164 
   166 		initialize: function() {
   165 		initialize: function() {
   167 			var self = this;
   166 			var self = this;
   168 
   167 
   169 			this.$search = $( '#widgets-search' );
   168 			this.$search = $( '#widgets-search' );
   170 
   169 
       
   170 			this.$clearResults = this.$el.find( '.clear-results' );
       
   171 
   171 			_.bindAll( this, 'close' );
   172 			_.bindAll( this, 'close' );
   172 
   173 
   173 			this.listenTo( this.collection, 'change', this.updateList );
   174 			this.listenTo( this.collection, 'change', this.updateList );
   174 
   175 
   175 			this.updateList();
   176 			this.updateList();
       
   177 
       
   178 			// Set the initial search count to the number of available widgets.
       
   179 			this.searchMatchesCount = this.collection.length;
   176 
   180 
   177 			// If the available widgets panel is open and the customize controls are
   181 			// If the available widgets panel is open and the customize controls are
   178 			// interacted with (i.e. available widgets panel is blurred) then close the
   182 			// interacted with (i.e. available widgets panel is blurred) then close the
   179 			// available widgets panel.
   183 			// available widgets panel. Also close on back button click.
   180 			$( '#customize-controls, .customize-overlay-close' ).on( 'click keydown', function( e ) {
   184 			$( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
   181 				var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
   185 				var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
   182 				if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
   186 				if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
   183 					self.close();
   187 					self.close();
   184 				}
   188 				}
   185 			} );
   189 			} );
   186 
   190 
       
   191 			// Clear the search results and trigger a `keyup` event to fire a new search.
       
   192 			this.$clearResults.on( 'click', function() {
       
   193 				self.$search.val( '' ).focus().trigger( 'keyup' );
       
   194 			} );
       
   195 
   187 			// Close the panel if the URL in the preview changes
   196 			// Close the panel if the URL in the preview changes
   188 			api.previewer.bind( 'url', this.close );
   197 			api.previewer.bind( 'url', this.close );
   189 		},
   198 		},
   190 
   199 
   191 		// Performs a search and handles selected widget
   200 		// Performs a search and handles selected widget
   192 		search: function( event ) {
   201 		search: function( event ) {
   193 			var firstVisible;
   202 			var firstVisible;
   194 
   203 
   195 			this.collection.doSearch( event.target.value );
   204 			this.collection.doSearch( event.target.value );
       
   205 			// Update the search matches count.
       
   206 			this.updateSearchMatchesCount();
       
   207 			// Announce how many search results.
       
   208 			this.announceSearchMatches();
   196 
   209 
   197 			// Remove a widget from being selected if it is no longer visible
   210 			// Remove a widget from being selected if it is no longer visible
   198 			if ( this.selected && ! this.selected.is( ':visible' ) ) {
   211 			if ( this.selected && ! this.selected.is( ':visible' ) ) {
   199 				this.selected.removeClass( 'selected' );
   212 				this.selected.removeClass( 'selected' );
   200 				this.selected = null;
   213 				this.selected = null;
   211 				firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
   224 				firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
   212 				if ( firstVisible.length ) {
   225 				if ( firstVisible.length ) {
   213 					this.select( firstVisible );
   226 					this.select( firstVisible );
   214 				}
   227 				}
   215 			}
   228 			}
   216 		},
   229 
       
   230 			// Toggle the clear search results button.
       
   231 			if ( '' !== event.target.value ) {
       
   232 				this.$clearResults.addClass( 'is-visible' );
       
   233 			} else if ( '' === event.target.value ) {
       
   234 				this.$clearResults.removeClass( 'is-visible' );
       
   235 			}
       
   236 
       
   237 			// Set a CSS class on the search container when there are no search results.
       
   238 			if ( ! this.searchMatchesCount ) {
       
   239 				this.$el.addClass( 'no-widgets-found' );
       
   240 			} else {
       
   241 				this.$el.removeClass( 'no-widgets-found' );
       
   242 			}
       
   243 		},
       
   244 
       
   245 		// Update the count of the available widgets that have the `search_matched` attribute.
       
   246 		updateSearchMatchesCount: function() {
       
   247 			this.searchMatchesCount = this.collection.where({ search_matched: true }).length;
       
   248 		},
       
   249 
       
   250 		// Send a message to the aria-live region to announce how many search results.
       
   251 		announceSearchMatches: _.debounce( function() {
       
   252 			var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ;
       
   253 
       
   254 			if ( ! this.searchMatchesCount ) {
       
   255 				message = l10n.noWidgetsFound;
       
   256 			}
       
   257 
       
   258 			wp.a11y.speak( message );
       
   259 		}, 500 ),
   217 
   260 
   218 		// Changes visibility of available widgets
   261 		// Changes visibility of available widgets
   219 		updateList: function() {
   262 		updateList: function() {
   220 			this.collection.each( function( widget ) {
   263 			this.collection.each( function( widget ) {
   221 				var widgetTpl = $( '#widget-tpl-' + widget.id );
   264 				var widgetTpl = $( '#widget-tpl-' + widget.id );
   284 			_( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
   327 			_( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
   285 				if ( control.params.is_wide ) {
   328 				if ( control.params.is_wide ) {
   286 					control.collapseForm();
   329 					control.collapseForm();
   287 				}
   330 				}
   288 			} );
   331 			} );
       
   332 
       
   333 			if ( api.section.has( 'publish_settings' ) ) {
       
   334 				api.section( 'publish_settings' ).collapse();
       
   335 			}
   289 
   336 
   290 			$( 'body' ).addClass( 'adding-widget' );
   337 			$( 'body' ).addClass( 'adding-widget' );
   291 
   338 
   292 			this.$el.find( '.selected' ).removeClass( 'selected' );
   339 			this.$el.find( '.selected' ).removeClass( 'selected' );
   293 
   340 
   364 				this.submit();
   411 				this.submit();
   365 			} else if ( isEsc ) {
   412 			} else if ( isEsc ) {
   366 				this.close( { returnFocus: true } );
   413 				this.close( { returnFocus: true } );
   367 			}
   414 			}
   368 
   415 
   369 			if ( isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
   416 			if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
   370 				this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
   417 				this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
   371 				event.preventDefault();
   418 				event.preventDefault();
   372 			}
   419 			}
   373 		}
   420 		}
   374 	});
   421 	});
   415 		},
   462 		},
   416 
   463 
   417 		/**
   464 		/**
   418 		 * @since 4.1.0
   465 		 * @since 4.1.0
   419 		 */
   466 		 */
   420 		initialize: function ( id, options ) {
   467 		initialize: function( id, options ) {
   421 			var control = this;
   468 			var control = this;
   422 			api.Control.prototype.initialize.call( control, id, options );
   469 
   423 			control.expanded = new api.Value();
   470 			control.widgetControlEmbedded = false;
       
   471 			control.widgetContentEmbedded = false;
       
   472 			control.expanded = new api.Value( false );
   424 			control.expandedArgumentsQueue = [];
   473 			control.expandedArgumentsQueue = [];
   425 			control.expanded.bind( function ( expanded ) {
   474 			control.expanded.bind( function( expanded ) {
   426 				var args = control.expandedArgumentsQueue.shift();
   475 				var args = control.expandedArgumentsQueue.shift();
   427 				args = $.extend( {}, control.defaultExpandedArguments, args );
   476 				args = $.extend( {}, control.defaultExpandedArguments, args );
   428 				control.onChangeExpanded( expanded, args );
   477 				control.onChangeExpanded( expanded, args );
   429 			});
   478 			});
   430 			control.expanded.set( false );
   479 			control.altNotice = true;
   431 		},
   480 
   432 
   481 			api.Control.prototype.initialize.call( control, id, options );
   433 		/**
   482 		},
   434 		 * Set up the control
   483 
       
   484 		/**
       
   485 		 * Set up the control.
       
   486 		 *
       
   487 		 * @since 3.9.0
   435 		 */
   488 		 */
   436 		ready: function() {
   489 		ready: function() {
   437 			this._setupModel();
   490 			var control = this;
   438 			this._setupWideWidget();
   491 
   439 			this._setupControlToggle();
   492 			/*
   440 			this._setupWidgetTitle();
   493 			 * Embed a placeholder once the section is expanded. The full widget
   441 			this._setupReorderUI();
   494 			 * form content will be embedded once the control itself is expanded,
   442 			this._setupHighlightEffects();
   495 			 * and at this point the widget-added event will be triggered.
   443 			this._setupUpdateUI();
   496 			 */
   444 			this._setupRemoveUI();
   497 			if ( ! control.section() ) {
       
   498 				control.embedWidgetControl();
       
   499 			} else {
       
   500 				api.section( control.section(), function( section ) {
       
   501 					var onExpanded = function( isExpanded ) {
       
   502 						if ( isExpanded ) {
       
   503 							control.embedWidgetControl();
       
   504 							section.expanded.unbind( onExpanded );
       
   505 						}
       
   506 					};
       
   507 					if ( section.expanded() ) {
       
   508 						onExpanded( true );
       
   509 					} else {
       
   510 						section.expanded.bind( onExpanded );
       
   511 					}
       
   512 				} );
       
   513 			}
       
   514 		},
       
   515 
       
   516 		/**
       
   517 		 * Embed the .widget element inside the li container.
       
   518 		 *
       
   519 		 * @since 4.4.0
       
   520 		 */
       
   521 		embedWidgetControl: function() {
       
   522 			var control = this, widgetControl;
       
   523 
       
   524 			if ( control.widgetControlEmbedded ) {
       
   525 				return;
       
   526 			}
       
   527 			control.widgetControlEmbedded = true;
       
   528 
       
   529 			widgetControl = $( control.params.widget_control );
       
   530 			control.container.append( widgetControl );
       
   531 
       
   532 			control._setupModel();
       
   533 			control._setupWideWidget();
       
   534 			control._setupControlToggle();
       
   535 
       
   536 			control._setupWidgetTitle();
       
   537 			control._setupReorderUI();
       
   538 			control._setupHighlightEffects();
       
   539 			control._setupUpdateUI();
       
   540 			control._setupRemoveUI();
       
   541 		},
       
   542 
       
   543 		/**
       
   544 		 * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
       
   545 		 *
       
   546 		 * @since 4.4.0
       
   547 		 */
       
   548 		embedWidgetContent: function() {
       
   549 			var control = this, widgetContent;
       
   550 
       
   551 			control.embedWidgetControl();
       
   552 			if ( control.widgetContentEmbedded ) {
       
   553 				return;
       
   554 			}
       
   555 			control.widgetContentEmbedded = true;
       
   556 
       
   557 			// Update the notification container element now that the widget content has been embedded.
       
   558 			control.notifications.container = control.getNotificationsContainerElement();
       
   559 			control.notifications.render();
       
   560 
       
   561 			widgetContent = $( control.params.widget_content );
       
   562 			control.container.find( '.widget-content:first' ).append( widgetContent );
   445 
   563 
   446 			/*
   564 			/*
   447 			 * Trigger widget-added event so that plugins can attach any event
   565 			 * Trigger widget-added event so that plugins can attach any event
   448 			 * listeners and dynamic UI elements.
   566 			 * listeners and dynamic UI elements.
   449 			 */
   567 			 */
   450 			$( document ).trigger( 'widget-added', [ this.container.find( '.widget:first' ) ] );
   568 			$( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
       
   569 
   451 		},
   570 		},
   452 
   571 
   453 		/**
   572 		/**
   454 		 * Handle changes to the setting
   573 		 * Handle changes to the setting
   455 		 */
   574 		 */
   456 		_setupModel: function() {
   575 		_setupModel: function() {
   457 			var self = this, rememberSavedWidgetId;
   576 			var self = this, rememberSavedWidgetId;
   458 
       
   459 			api.Widgets.savedWidgetIds = api.Widgets.savedWidgetIds || [];
       
   460 
   577 
   461 			// Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
   578 			// Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
   462 			rememberSavedWidgetId = function() {
   579 			rememberSavedWidgetId = function() {
   463 				api.Widgets.savedWidgetIds[self.params.widget_id] = true;
   580 				api.Widgets.savedWidgetIds[self.params.widget_id] = true;
   464 			};
   581 			};
   482 		 */
   599 		 */
   483 		_setupWideWidget: function() {
   600 		_setupWideWidget: function() {
   484 			var self = this, $widgetInside, $widgetForm, $customizeSidebar,
   601 			var self = this, $widgetInside, $widgetForm, $customizeSidebar,
   485 				$themeControlsContainer, positionWidget;
   602 				$themeControlsContainer, positionWidget;
   486 
   603 
   487 			if ( ! this.params.is_wide ) {
   604 			if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) {
   488 				return;
   605 				return;
   489 			}
   606 			}
   490 
   607 
   491 			$widgetInside = this.container.find( '.widget-inside' );
   608 			$widgetInside = this.container.find( '.widget-inside' );
   492 			$widgetForm = $widgetInside.find( '> .form' );
   609 			$widgetForm = $widgetInside.find( '> .form' );
   493 			$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
   610 			$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
   494 			this.container.addClass( 'wide-widget-control' );
   611 			this.container.addClass( 'wide-widget-control' );
   495 
   612 
   496 			this.container.find( '.widget-content:first' ).css( {
   613 			this.container.find( '.form:first' ).css( {
   497 				'max-width': this.params.width,
   614 				'max-width': this.params.width,
   498 				'min-height': this.params.height
   615 				'min-height': this.params.height
   499 			} );
   616 			} );
   500 
   617 
   501 			/**
   618 			/**
   595 		/**
   712 		/**
   596 		 * Set up the widget-reorder-nav
   713 		 * Set up the widget-reorder-nav
   597 		 */
   714 		 */
   598 		_setupReorderUI: function() {
   715 		_setupReorderUI: function() {
   599 			var self = this, selectSidebarItem, $moveWidgetArea,
   716 			var self = this, selectSidebarItem, $moveWidgetArea,
   600 				$reorderNav, updateAvailableSidebars;
   717 				$reorderNav, updateAvailableSidebars, template;
   601 
   718 
   602 			/**
   719 			/**
   603 			 * select the provided sidebar list item in the move widget area
   720 			 * select the provided sidebar list item in the move widget area
   604 			 *
   721 			 *
   605 			 * @param {jQuery} li
   722 			 * @param {jQuery} li
   613 
   730 
   614 			/**
   731 			/**
   615 			 * Add the widget reordering elements to the widget control
   732 			 * Add the widget reordering elements to the widget control
   616 			 */
   733 			 */
   617 			this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
   734 			this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
   618 			$moveWidgetArea = $(
   735 
   619 				_.template( api.Widgets.data.tpl.moveWidgetArea, {
   736 
       
   737 			template = _.template( api.Widgets.data.tpl.moveWidgetArea );
       
   738 			$moveWidgetArea = $( template( {
   620 					sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
   739 					sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
   621 				} )
   740 				} )
   622 			);
   741 			);
   623 			this.container.find( '.widget-top' ).after( $moveWidgetArea );
   742 			this.container.find( '.widget-top' ).after( $moveWidgetArea );
   624 
   743 
   764 
   883 
   765 			// Configure update button
   884 			// Configure update button
   766 			$saveBtn = this.container.find( '.widget-control-save' );
   885 			$saveBtn = this.container.find( '.widget-control-save' );
   767 			$saveBtn.val( l10n.saveBtnLabel );
   886 			$saveBtn.val( l10n.saveBtnLabel );
   768 			$saveBtn.attr( 'title', l10n.saveBtnTooltip );
   887 			$saveBtn.attr( 'title', l10n.saveBtnTooltip );
   769 			$saveBtn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
   888 			$saveBtn.removeClass( 'button-primary' );
   770 			$saveBtn.on( 'click', function( e ) {
   889 			$saveBtn.on( 'click', function( e ) {
   771 				e.preventDefault();
   890 				e.preventDefault();
   772 				self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
   891 				self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
   773 			} );
   892 			} );
   774 
   893 
   784 				}
   903 				}
   785 			} );
   904 			} );
   786 
   905 
   787 			// Handle widgets that support live previews
   906 			// Handle widgets that support live previews
   788 			$widgetContent.on( 'change input propertychange', ':input', function( e ) {
   907 			$widgetContent.on( 'change input propertychange', ':input', function( e ) {
   789 				if ( self.liveUpdateMode ) {
   908 				if ( ! self.liveUpdateMode ) {
   790 					if ( e.type === 'change' ) {
   909 					return;
   791 						self.updateWidget();
   910 				}
   792 					} else if ( this.checkValidity && this.checkValidity() ) {
   911 				if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
   793 						updateWidgetDebounced();
   912 					updateWidgetDebounced();
   794 					}
       
   795 				}
   913 				}
   796 			} );
   914 			} );
   797 
   915 
   798 			// Remove loading indicators when the setting is saved and the preview updates
   916 			// Remove loading indicators when the setting is saved and the preview updates
   799 			this.setting.previewer.channel.bind( 'synced', function() {
   917 			this.setting.previewer.channel.bind( 'synced', function() {
   840 		 */
   958 		 */
   841 		_setupRemoveUI: function() {
   959 		_setupRemoveUI: function() {
   842 			var self = this, $removeBtn, replaceDeleteWithRemove;
   960 			var self = this, $removeBtn, replaceDeleteWithRemove;
   843 
   961 
   844 			// Configure remove button
   962 			// Configure remove button
   845 			$removeBtn = this.container.find( 'a.widget-control-remove' );
   963 			$removeBtn = this.container.find( '.widget-control-remove' );
   846 			$removeBtn.on( 'click', function( e ) {
   964 			$removeBtn.on( 'click', function( e ) {
   847 				e.preventDefault();
   965 				e.preventDefault();
   848 
   966 
   849 				// Find an adjacent element to add focus to when this widget goes away
   967 				// Find an adjacent element to add focus to when this widget goes away
   850 				var $adjacentFocusTarget;
   968 				var $adjacentFocusTarget;
   876 					$adjacentFocusTarget.focus(); // keyboard accessibility
   994 					$adjacentFocusTarget.focus(); // keyboard accessibility
   877 				} );
   995 				} );
   878 			} );
   996 			} );
   879 
   997 
   880 			replaceDeleteWithRemove = function() {
   998 			replaceDeleteWithRemove = function() {
   881 				$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete"
   999 				$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete"
   882 				$removeBtn.attr( 'title', l10n.removeBtnTooltip );
  1000 				$removeBtn.attr( 'title', l10n.removeBtnTooltip );
   883 			};
  1001 			};
   884 
  1002 
   885 			if ( this.params.is_new ) {
  1003 			if ( this.params.is_new ) {
   886 				api.bind( 'saved', replaceDeleteWithRemove );
  1004 				api.bind( 'saved', replaceDeleteWithRemove );
   892 		/**
  1010 		/**
   893 		 * Find all inputs in a widget container that should be considered when
  1011 		 * Find all inputs in a widget container that should be considered when
   894 		 * comparing the loaded form with the sanitized form, whose fields will
  1012 		 * comparing the loaded form with the sanitized form, whose fields will
   895 		 * be aligned to copy the sanitized over. The elements returned by this
  1013 		 * be aligned to copy the sanitized over. The elements returned by this
   896 		 * are passed into this._getInputsSignature(), and they are iterated
  1014 		 * are passed into this._getInputsSignature(), and they are iterated
   897 		 * over when copying sanitized values over to the the form loaded.
  1015 		 * over when copying sanitized values over to the form loaded.
   898 		 *
  1016 		 *
   899 		 * @param {jQuery} container element in which to look for inputs
  1017 		 * @param {jQuery} container element in which to look for inputs
   900 		 * @returns {jQuery} inputs
  1018 		 * @returns {jQuery} inputs
   901 		 * @private
  1019 		 * @private
   902 		 */
  1020 		 */
  1007 		 */
  1125 		 */
  1008 		updateWidget: function( args ) {
  1126 		updateWidget: function( args ) {
  1009 			var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
  1127 			var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
  1010 				updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
  1128 				updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
  1011 
  1129 
       
  1130 			// The updateWidget logic requires that the form fields to be fully present.
       
  1131 			self.embedWidgetContent();
       
  1132 
  1012 			args = $.extend( {
  1133 			args = $.extend( {
  1013 				instance: null,
  1134 				instance: null,
  1014 				complete: null,
  1135 				complete: null,
  1015 				ignoreActiveElement: false
  1136 				ignoreActiveElement: false
  1016 			}, args );
  1137 			}, args );
  1037 			}
  1158 			}
  1038 
  1159 
  1039 			params = {};
  1160 			params = {};
  1040 			params.action = 'update-widget';
  1161 			params.action = 'update-widget';
  1041 			params.wp_customize = 'on';
  1162 			params.wp_customize = 'on';
  1042 			params.nonce = api.Widgets.data.nonce;
  1163 			params.nonce = api.settings.nonce['update-widget'];
  1043 			params.theme = api.settings.theme.stylesheet;
  1164 			params.customize_theme = api.settings.theme.stylesheet;
       
  1165 			params.customized = wp.customize.previewer.query().customized;
  1044 
  1166 
  1045 			data = $.param( params );
  1167 			data = $.param( params );
  1046 			$inputs = this._getInputs( $widgetContent );
  1168 			$inputs = this._getInputs( $widgetContent );
  1047 
  1169 
  1048 			// Store the value we're submitting in data so that when the response comes back,
  1170 			// Store the value we're submitting in data so that when the response comes back,
  1251 		 *
  1373 		 *
  1252 		 * @param {Boolean} expanded
  1374 		 * @param {Boolean} expanded
  1253 		 * @param {Object} args  merged on top of this.defaultActiveArguments
  1375 		 * @param {Object} args  merged on top of this.defaultActiveArguments
  1254 		 */
  1376 		 */
  1255 		onChangeExpanded: function ( expanded, args ) {
  1377 		onChangeExpanded: function ( expanded, args ) {
  1256 			var self = this, $widget, $inside, complete, prevComplete;
  1378 			var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
       
  1379 
       
  1380 			self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
       
  1381 			if ( expanded ) {
       
  1382 				self.embedWidgetContent();
       
  1383 			}
  1257 
  1384 
  1258 			// If the expanded state is unchanged only manipulate container expanded states
  1385 			// If the expanded state is unchanged only manipulate container expanded states
  1259 			if ( args.unchanged ) {
  1386 			if ( args.unchanged ) {
  1260 				if ( expanded ) {
  1387 				if ( expanded ) {
  1261 					api.Control.prototype.expand.call( self, {
  1388 					api.Control.prototype.expand.call( self, {
  1265 				return;
  1392 				return;
  1266 			}
  1393 			}
  1267 
  1394 
  1268 			$widget = this.container.find( 'div.widget:first' );
  1395 			$widget = this.container.find( 'div.widget:first' );
  1269 			$inside = $widget.find( '.widget-inside:first' );
  1396 			$inside = $widget.find( '.widget-inside:first' );
  1270 
  1397 			$toggleBtn = this.container.find( '.widget-top button.widget-action' );
  1271 			if ( expanded ) {
  1398 
  1272 
  1399 			expandControl = function() {
  1273 				self.expandControlSection();
       
  1274 
  1400 
  1275 				// Close all other widget controls before expanding this one
  1401 				// Close all other widget controls before expanding this one
  1276 				api.control.each( function( otherControl ) {
  1402 				api.control.each( function( otherControl ) {
  1277 					if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1403 					if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1278 						otherControl.collapse();
  1404 						otherControl.collapse();
  1280 				} );
  1406 				} );
  1281 
  1407 
  1282 				complete = function() {
  1408 				complete = function() {
  1283 					self.container.removeClass( 'expanding' );
  1409 					self.container.removeClass( 'expanding' );
  1284 					self.container.addClass( 'expanded' );
  1410 					self.container.addClass( 'expanded' );
       
  1411 					$widget.addClass( 'open' );
       
  1412 					$toggleBtn.attr( 'aria-expanded', 'true' );
  1285 					self.container.trigger( 'expanded' );
  1413 					self.container.trigger( 'expanded' );
  1286 				};
  1414 				};
  1287 				if ( args.completeCallback ) {
  1415 				if ( args.completeCallback ) {
  1288 					prevComplete = complete;
  1416 					prevComplete = complete;
  1289 					complete = function () {
  1417 					complete = function () {
  1298 					$inside.slideDown( args.duration, complete );
  1426 					$inside.slideDown( args.duration, complete );
  1299 				}
  1427 				}
  1300 
  1428 
  1301 				self.container.trigger( 'expand' );
  1429 				self.container.trigger( 'expand' );
  1302 				self.container.addClass( 'expanding' );
  1430 				self.container.addClass( 'expanding' );
       
  1431 			};
       
  1432 
       
  1433 			if ( expanded ) {
       
  1434 				if ( api.section.has( self.section() ) ) {
       
  1435 					api.section( self.section() ).expand( {
       
  1436 						completeCallback: expandControl
       
  1437 					} );
       
  1438 				} else {
       
  1439 					expandControl();
       
  1440 				}
  1303 			} else {
  1441 			} else {
  1304 
       
  1305 				complete = function() {
  1442 				complete = function() {
  1306 					self.container.removeClass( 'collapsing' );
  1443 					self.container.removeClass( 'collapsing' );
  1307 					self.container.removeClass( 'expanded' );
  1444 					self.container.removeClass( 'expanded' );
       
  1445 					$widget.removeClass( 'open' );
       
  1446 					$toggleBtn.attr( 'aria-expanded', 'false' );
  1308 					self.container.trigger( 'collapsed' );
  1447 					self.container.trigger( 'collapsed' );
  1309 				};
  1448 				};
  1310 				if ( args.completeCallback ) {
  1449 				if ( args.completeCallback ) {
  1311 					prevComplete = complete;
  1450 					prevComplete = complete;
  1312 					complete = function () {
  1451 					complete = function () {
  1426 			setTimeout( function() {
  1565 			setTimeout( function() {
  1427 				$target.removeClass( 'highlighted' );
  1566 				$target.removeClass( 'highlighted' );
  1428 			}, 500 );
  1567 			}, 500 );
  1429 		}
  1568 		}
  1430 	} );
  1569 	} );
       
  1570 
       
  1571 	/**
       
  1572 	 * wp.customize.Widgets.WidgetsPanel
       
  1573 	 *
       
  1574 	 * Customizer panel containing the widget area sections.
       
  1575 	 *
       
  1576 	 * @since 4.4.0
       
  1577 	 */
       
  1578 	api.Widgets.WidgetsPanel = api.Panel.extend({
       
  1579 
       
  1580 		/**
       
  1581 		 * Add and manage the display of the no-rendered-areas notice.
       
  1582 		 *
       
  1583 		 * @since 4.4.0
       
  1584 		 */
       
  1585 		ready: function () {
       
  1586 			var panel = this;
       
  1587 
       
  1588 			api.Panel.prototype.ready.call( panel );
       
  1589 
       
  1590 			panel.deferred.embedded.done(function() {
       
  1591 				var panelMetaContainer, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice;
       
  1592 				panelMetaContainer = panel.container.find( '.panel-meta' );
       
  1593 
       
  1594 				// @todo This should use the Notifications API introduced to panels. See <https://core.trac.wordpress.org/ticket/38794>.
       
  1595 				noticeContainer = $( '<div></div>', {
       
  1596 					'class': 'no-widget-areas-rendered-notice'
       
  1597 				});
       
  1598 				panelMetaContainer.append( noticeContainer );
       
  1599 
       
  1600 				/**
       
  1601 				 * Get the number of active sections in the panel.
       
  1602 				 *
       
  1603 				 * @return {number} Number of active sidebar sections.
       
  1604 				 */
       
  1605 				getActiveSectionCount = function() {
       
  1606 					return _.filter( panel.sections(), function( section ) {
       
  1607 						return section.active();
       
  1608 					} ).length;
       
  1609 				};
       
  1610 
       
  1611 				/**
       
  1612 				 * Determine whether or not the notice should be displayed.
       
  1613 				 *
       
  1614 				 * @return {boolean}
       
  1615 				 */
       
  1616 				shouldShowNotice = function() {
       
  1617 					var activeSectionCount = getActiveSectionCount();
       
  1618 					if ( 0 === activeSectionCount ) {
       
  1619 						return true;
       
  1620 					} else {
       
  1621 						return activeSectionCount !== api.Widgets.data.registeredSidebars.length;
       
  1622 					}
       
  1623 				};
       
  1624 
       
  1625 				/**
       
  1626 				 * Update the notice.
       
  1627 				 *
       
  1628 				 * @returns {void}
       
  1629 				 */
       
  1630 				updateNotice = function() {
       
  1631 					var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
       
  1632 					noticeContainer.empty();
       
  1633 
       
  1634 					registeredAreaCount = api.Widgets.data.registeredSidebars.length;
       
  1635 					if ( activeSectionCount !== registeredAreaCount ) {
       
  1636 
       
  1637 						if ( 0 !== activeSectionCount ) {
       
  1638 							nonRenderedAreaCount = registeredAreaCount - activeSectionCount;
       
  1639 							someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ];
       
  1640 						} else {
       
  1641 							someRenderedMessage = l10n.noAreasShown;
       
  1642 						}
       
  1643 						if ( someRenderedMessage ) {
       
  1644 							noticeContainer.append( $( '<p></p>', {
       
  1645 								text: someRenderedMessage
       
  1646 							} ) );
       
  1647 						}
       
  1648 
       
  1649 						noticeContainer.append( $( '<p></p>', {
       
  1650 							text: l10n.navigatePreview
       
  1651 						} ) );
       
  1652 					}
       
  1653 				};
       
  1654 				updateNotice();
       
  1655 
       
  1656 				/*
       
  1657 				 * Set the initial visibility state for rendered notice.
       
  1658 				 * Update the visibility of the notice whenever a reflow happens.
       
  1659 				 */
       
  1660 				noticeContainer.toggle( shouldShowNotice() );
       
  1661 				api.previewer.deferred.active.done( function () {
       
  1662 					noticeContainer.toggle( shouldShowNotice() );
       
  1663 				});
       
  1664 				api.bind( 'pane-contents-reflowed', function() {
       
  1665 					var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0;
       
  1666 					updateNotice();
       
  1667 					if ( shouldShowNotice() ) {
       
  1668 						noticeContainer.slideDown( duration );
       
  1669 					} else {
       
  1670 						noticeContainer.slideUp( duration );
       
  1671 					}
       
  1672 				});
       
  1673 			});
       
  1674 		},
       
  1675 
       
  1676 		/**
       
  1677 		 * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas).
       
  1678 		 *
       
  1679 		 * This ensures that the widgets panel appears even when there are no
       
  1680 		 * sidebars displayed on the URL currently being previewed.
       
  1681 		 *
       
  1682 		 * @since 4.4.0
       
  1683 		 *
       
  1684 		 * @returns {boolean}
       
  1685 		 */
       
  1686 		isContextuallyActive: function() {
       
  1687 			var panel = this;
       
  1688 			return panel.active();
       
  1689 		}
       
  1690 	});
  1431 
  1691 
  1432 	/**
  1692 	/**
  1433 	 * wp.customize.Widgets.SidebarSection
  1693 	 * wp.customize.Widgets.SidebarSection
  1434 	 *
  1694 	 *
  1435 	 * Customizer section representing a widget area widget
  1695 	 * Customizer section representing a widget area widget
  1602 			 */
  1862 			 */
  1603 			this.$sectionContent.sortable( {
  1863 			this.$sectionContent.sortable( {
  1604 				items: '> .customize-control-widget_form',
  1864 				items: '> .customize-control-widget_form',
  1605 				handle: '.widget-top',
  1865 				handle: '.widget-top',
  1606 				axis: 'y',
  1866 				axis: 'y',
       
  1867 				tolerance: 'pointer',
  1607 				connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
  1868 				connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
  1608 				update: function() {
  1869 				update: function() {
  1609 					var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
  1870 					var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
  1610 
  1871 
  1611 					widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) {
  1872 					widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) {
  1639 			});
  1900 			});
  1640 
  1901 
  1641 			/**
  1902 			/**
  1642 			 * Keyboard-accessible reordering
  1903 			 * Keyboard-accessible reordering
  1643 			 */
  1904 			 */
  1644 			this.container.find( '.reorder-toggle' ).on( 'click keydown', function( event ) {
  1905 			this.container.find( '.reorder-toggle' ).on( 'click', function() {
  1645 				if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
       
  1646 					return;
       
  1647 				}
       
  1648 
       
  1649 				self.toggleReordering( ! self.isReordering );
  1906 				self.toggleReordering( ! self.isReordering );
  1650 			} );
  1907 			} );
  1651 		},
  1908 		},
  1652 
  1909 
  1653 		/**
  1910 		/**
  1654 		 * Set up UI for adding a new widget
  1911 		 * Set up UI for adding a new widget
  1655 		 */
  1912 		 */
  1656 		_setupAddition: function() {
  1913 		_setupAddition: function() {
  1657 			var self = this;
  1914 			var self = this;
  1658 
  1915 
  1659 			this.container.find( '.add-new-widget' ).on( 'click keydown', function( event ) {
  1916 			this.container.find( '.add-new-widget' ).on( 'click', function() {
  1660 				if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
  1917 				var addNewWidgetBtn = $( this );
  1661 					return;
       
  1662 				}
       
  1663 
  1918 
  1664 				if ( self.$sectionContent.hasClass( 'reordering' ) ) {
  1919 				if ( self.$sectionContent.hasClass( 'reordering' ) ) {
  1665 					return;
  1920 					return;
  1666 				}
  1921 				}
  1667 
  1922 
  1668 				if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
  1923 				if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
       
  1924 					addNewWidgetBtn.attr( 'aria-expanded', 'true' );
  1669 					api.Widgets.availableWidgetsPanel.open( self );
  1925 					api.Widgets.availableWidgetsPanel.open( self );
  1670 				} else {
  1926 				} else {
       
  1927 					addNewWidgetBtn.attr( 'aria-expanded', 'false' );
  1671 					api.Widgets.availableWidgetsPanel.close();
  1928 					api.Widgets.availableWidgetsPanel.close();
  1672 				}
  1929 				}
  1673 			} );
  1930 			} );
  1674 		},
  1931 		},
  1675 
  1932 
  1683 				if ( widgetControl ) {
  1940 				if ( widgetControl ) {
  1684 					widgetControls.push( widgetControl );
  1941 					widgetControls.push( widgetControl );
  1685 				}
  1942 				}
  1686 			});
  1943 			});
  1687 
  1944 
  1688 			if ( ! widgetControls.length ) {
  1945 			if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) {
  1689 				this.container.find( '.reorder-toggle' ).hide();
  1946 				this.container.find( '.reorder-toggle' ).hide();
  1690 				return;
  1947 				return;
  1691 			} else {
  1948 			} else {
  1692 				this.container.find( '.reorder-toggle' ).show();
  1949 				this.container.find( '.reorder-toggle' ).show();
  1693 			}
  1950 			}
  1719 		 * @param {Boolean} showOrHide to enable/disable reordering
  1976 		 * @param {Boolean} showOrHide to enable/disable reordering
  1720 		 *
  1977 		 *
  1721 		 * @todo We should have a reordering state instead and rename this to onChangeReordering
  1978 		 * @todo We should have a reordering state instead and rename this to onChangeReordering
  1722 		 */
  1979 		 */
  1723 		toggleReordering: function( showOrHide ) {
  1980 		toggleReordering: function( showOrHide ) {
       
  1981 			var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
       
  1982 				reorderBtn = this.container.find( '.reorder-toggle' ),
       
  1983 				widgetsTitle = this.$sectionContent.find( '.widget-title' );
       
  1984 
  1724 			showOrHide = Boolean( showOrHide );
  1985 			showOrHide = Boolean( showOrHide );
  1725 
  1986 
  1726 			if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
  1987 			if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
  1727 				return;
  1988 				return;
  1728 			}
  1989 			}
  1733 			if ( showOrHide ) {
  1994 			if ( showOrHide ) {
  1734 				_( this.getWidgetFormControls() ).each( function( formControl ) {
  1995 				_( this.getWidgetFormControls() ).each( function( formControl ) {
  1735 					formControl.collapse();
  1996 					formControl.collapse();
  1736 				} );
  1997 				} );
  1737 
  1998 
  1738 				this.$sectionContent.find( '.first-widget .move-widget' ).focus();
  1999 				addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  1739 				this.$sectionContent.find( '.add-new-widget' ).prop( 'tabIndex', -1 );
  2000 				reorderBtn.attr( 'aria-label', l10n.reorderLabelOff );
       
  2001 				wp.a11y.speak( l10n.reorderModeOn );
       
  2002 				// Hide widget titles while reordering: title is already in the reorder controls.
       
  2003 				widgetsTitle.attr( 'aria-hidden', 'true' );
  1740 			} else {
  2004 			} else {
  1741 				this.$sectionContent.find( '.add-new-widget' ).prop( 'tabIndex', 0 );
  2005 				addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
       
  2006 				reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
       
  2007 				wp.a11y.speak( l10n.reorderModeOff );
       
  2008 				widgetsTitle.attr( 'aria-hidden', 'false' );
  1742 			}
  2009 			}
  1743 		},
  2010 		},
  1744 
  2011 
  1745 		/**
  2012 		/**
  1746 		 * Get the widget_form Customize controls associated with the current sidebar.
  2013 		 * Get the widget_form Customize controls associated with the current sidebar.
  1747 		 *
  2014 		 *
  1748 		 * @since 3.9
  2015 		 * @since 3.9.0
  1749 		 * @return {wp.customize.controlConstructor.widget_form[]}
  2016 		 * @return {wp.customize.controlConstructor.widget_form[]}
  1750 		 */
  2017 		 */
  1751 		getWidgetFormControls: function() {
  2018 		getWidgetFormControls: function() {
  1752 			var formControls = [];
  2019 			var formControls = [];
  1753 
  2020 
  1824 
  2091 
  1825 			// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
  2092 			// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
  1826 			isExistingWidget = api.has( settingId );
  2093 			isExistingWidget = api.has( settingId );
  1827 			if ( ! isExistingWidget ) {
  2094 			if ( ! isExistingWidget ) {
  1828 				settingArgs = {
  2095 				settingArgs = {
  1829 					transport: 'refresh',
  2096 					transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
  1830 					previewer: this.setting.previewer
  2097 					previewer: this.setting.previewer
  1831 				};
  2098 				};
  1832 				setting = api.create( settingId, settingId, '', settingArgs );
  2099 				setting = api.create( settingId, settingId, '', settingArgs );
  1833 				setting.set( {} ); // mark dirty, changing from '' to {}
  2100 				setting.set( {} ); // mark dirty, changing from '' to {}
  1834 			}
  2101 			}
  1835 
  2102 
  1836 			controlConstructor = api.controlConstructor[controlType];
  2103 			controlConstructor = api.controlConstructor[controlType];
  1837 			widgetFormControl = new controlConstructor( settingId, {
  2104 			widgetFormControl = new controlConstructor( settingId, {
  1838 				params: {
  2105 				settings: {
  1839 					settings: {
  2106 					'default': settingId
  1840 						'default': settingId
       
  1841 					},
       
  1842 					content: controlContainer,
       
  1843 					sidebar_id: self.params.sidebar_id,
       
  1844 					widget_id: widgetId,
       
  1845 					widget_id_base: widget.get( 'id_base' ),
       
  1846 					type: controlType,
       
  1847 					is_new: ! isExistingWidget,
       
  1848 					width: widget.get( 'width' ),
       
  1849 					height: widget.get( 'height' ),
       
  1850 					is_wide: widget.get( 'is_wide' )
       
  1851 				},
  2107 				},
  1852 				previewer: self.setting.previewer
  2108 				content: controlContainer,
  1853 			} );
  2109 				sidebar_id: self.params.sidebar_id,
  1854 			api.control.add( settingId, widgetFormControl );
  2110 				widget_id: widgetId,
       
  2111 				widget_id_base: widget.get( 'id_base' ),
       
  2112 				type: controlType,
       
  2113 				is_new: ! isExistingWidget,
       
  2114 				width: widget.get( 'width' ),
       
  2115 				height: widget.get( 'height' ),
       
  2116 				is_wide: widget.get( 'is_wide' )
       
  2117 			} );
       
  2118 			api.control.add( widgetFormControl );
  1855 
  2119 
  1856 			// Make sure widget is removed from the other sidebars
  2120 			// Make sure widget is removed from the other sidebars
  1857 			api.each( function( otherSetting ) {
  2121 			api.each( function( otherSetting ) {
  1858 				if ( otherSetting.id === self.setting.id ) {
  2122 				if ( otherSetting.id === self.setting.id ) {
  1859 					return;
  2123 					return;
  1889 
  2153 
  1890 			return widgetFormControl;
  2154 			return widgetFormControl;
  1891 		}
  2155 		}
  1892 	} );
  2156 	} );
  1893 
  2157 
  1894 	// Register models for custom section and control types
  2158 	// Register models for custom panel, section, and control types
       
  2159 	$.extend( api.panelConstructor, {
       
  2160 		widgets: api.Widgets.WidgetsPanel
       
  2161 	});
  1895 	$.extend( api.sectionConstructor, {
  2162 	$.extend( api.sectionConstructor, {
  1896 		sidebar: api.Widgets.SidebarSection
  2163 		sidebar: api.Widgets.SidebarSection
  1897 	});
  2164 	});
  1898 	$.extend( api.controlConstructor, {
  2165 	$.extend( api.controlConstructor, {
  1899 		widget_form: api.Widgets.WidgetControl,
  2166 		widget_form: api.Widgets.WidgetControl,
  1900 		sidebar_widgets: api.Widgets.SidebarControl
  2167 		sidebar_widgets: api.Widgets.SidebarControl
  1901 	});
       
  1902 
       
  1903 	// Refresh the nonce if login sends updated nonces over.
       
  1904 	api.bind( 'nonce-refresh', function( nonces ) {
       
  1905 		api.Widgets.data.nonce = nonces['update-widget'];
       
  1906 	});
  2168 	});
  1907 
  2169 
  1908 	/**
  2170 	/**
  1909 	 * Init Customizer for widgets.
  2171 	 * Init Customizer for widgets.
  1910 	 */
  2172 	 */
  1981 			}
  2243 			}
  1982 		} );
  2244 		} );
  1983 
  2245 
  1984 		return foundControl;
  2246 		return foundControl;
  1985 	};
  2247 	};
       
  2248 
       
  2249 	/**
       
  2250 	 * Initialize Edit Menu button in Nav Menu widget.
       
  2251 	 */
       
  2252 	$( document ).on( 'widget-added', function( event, widgetContainer ) {
       
  2253 		var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton;
       
  2254 		parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() );
       
  2255 		if ( 'nav_menu' !== parsedWidgetId.id_base ) {
       
  2256 			return;
       
  2257 		}
       
  2258 		widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' );
       
  2259 		if ( ! widgetControl ) {
       
  2260 			return;
       
  2261 		}
       
  2262 		navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' );
       
  2263 		editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' );
       
  2264 		if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) {
       
  2265 			return;
       
  2266 		}
       
  2267 		navMenuSelect.on( 'change', function() {
       
  2268 			if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) {
       
  2269 				editMenuButton.parent().show();
       
  2270 			} else {
       
  2271 				editMenuButton.parent().hide();
       
  2272 			}
       
  2273 		});
       
  2274 		editMenuButton.on( 'click', function() {
       
  2275 			var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' );
       
  2276 			if ( section ) {
       
  2277 				focusConstructWithBreadcrumb( section, widgetControl );
       
  2278 			}
       
  2279 		} );
       
  2280 	} );
       
  2281 
       
  2282 	/**
       
  2283 	 * Focus (expand) one construct and then focus on another construct after the first is collapsed.
       
  2284 	 *
       
  2285 	 * This overrides the back button to serve the purpose of breadcrumb navigation.
       
  2286 	 *
       
  2287 	 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus.
       
  2288 	 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus.
       
  2289 	 */
       
  2290 	function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) {
       
  2291 		focusConstruct.focus();
       
  2292 		function onceCollapsed( isExpanded ) {
       
  2293 			if ( ! isExpanded ) {
       
  2294 				focusConstruct.expanded.unbind( onceCollapsed );
       
  2295 				returnConstruct.focus();
       
  2296 			}
       
  2297 		}
       
  2298 		focusConstruct.expanded.bind( onceCollapsed );
       
  2299 	}
  1986 
  2300 
  1987 	/**
  2301 	/**
  1988 	 * @param {String} widgetId
  2302 	 * @param {String} widgetId
  1989 	 * @returns {Object}
  2303 	 * @returns {Object}
  1990 	 */
  2304 	 */