wp/wp-admin/js/customize-widgets.js
changeset 16 a86126ab1dd4
parent 9 177826044cd9
child 18 be944660c56a
equal deleted inserted replaced
15:3d4e9c994f10 16:a86126ab1dd4
    15 	 * @namespace wp.customize.Widgets
    15 	 * @namespace wp.customize.Widgets
    16 	 */
    16 	 */
    17 	api.Widgets = api.Widgets || {};
    17 	api.Widgets = api.Widgets || {};
    18 	api.Widgets.savedWidgetIds = {};
    18 	api.Widgets.savedWidgetIds = {};
    19 
    19 
    20 	// Link settings
    20 	// Link settings.
    21 	api.Widgets.data = _wpCustomizeWidgetsSettings || {};
    21 	api.Widgets.data = _wpCustomizeWidgetsSettings || {};
    22 	l10n = api.Widgets.data.l10n;
    22 	l10n = api.Widgets.data.l10n;
    23 
    23 
    24 	/**
    24 	/**
    25 	 * wp.customize.Widgets.WidgetModel
    25 	 * wp.customize.Widgets.WidgetModel
    57 	 */
    57 	 */
    58 	api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{
    58 	api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{
    59 		model: api.Widgets.WidgetModel,
    59 		model: api.Widgets.WidgetModel,
    60 
    60 
    61 		// Controls searching on the current widget collection
    61 		// Controls searching on the current widget collection
    62 		// and triggers an update event
    62 		// and triggers an update event.
    63 		doSearch: function( value ) {
    63 		doSearch: function( value ) {
    64 
    64 
    65 			// Don't do anything if we've already done this search
    65 			// Don't do anything if we've already done this search.
    66 			// Useful because the search handler fires multiple times per keystroke
    66 			// Useful because the search handler fires multiple times per keystroke.
    67 			if ( this.terms === value ) {
    67 			if ( this.terms === value ) {
    68 				return;
    68 				return;
    69 			}
    69 			}
    70 
    70 
    71 			// Updates terms with the value passed
    71 			// Updates terms with the value passed.
    72 			this.terms = value;
    72 			this.terms = value;
    73 
    73 
    74 			// If we have terms, run a search...
    74 			// If we have terms, run a search...
    75 			if ( this.terms.length > 0 ) {
    75 			if ( this.terms.length > 0 ) {
    76 				this.search( this.terms );
    76 				this.search( this.terms );
    82 					widget.set( 'search_matched', true );
    82 					widget.set( 'search_matched', true );
    83 				} );
    83 				} );
    84 			}
    84 			}
    85 		},
    85 		},
    86 
    86 
    87 		// Performs a search within the collection
    87 		// Performs a search within the collection.
    88 		// @uses RegExp
    88 		// @uses RegExp
    89 		search: function( term ) {
    89 		search: function( term ) {
    90 			var match, haystack;
    90 			var match, haystack;
    91 
    91 
    92 			// Escape the term string for RegExp meta characters
    92 			// Escape the term string for RegExp meta characters.
    93 			term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
    93 			term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
    94 
    94 
    95 			// Consider spaces as word delimiters and match the whole string
    95 			// Consider spaces as word delimiters and match the whole string
    96 			// so matching terms can be combined
    96 			// so matching terms can be combined.
    97 			term = term.replace( / /g, ')(?=.*' );
    97 			term = term.replace( / /g, ')(?=.*' );
    98 			match = new RegExp( '^(?=.*' + term + ').+', 'i' );
    98 			match = new RegExp( '^(?=.*' + term + ').+', 'i' );
    99 
    99 
   100 			this.each( function ( data ) {
   100 			this.each( function ( data ) {
   101 				haystack = [ data.get( 'name' ), data.get( 'id' ), data.get( 'description' ) ].join( ' ' );
   101 				haystack = [ data.get( 'name' ), data.get( 'id' ), data.get( 'description' ) ].join( ' ' );
   148 			'click .widget-tpl' : '_submit',
   148 			'click .widget-tpl' : '_submit',
   149 			'keypress .widget-tpl' : '_submit',
   149 			'keypress .widget-tpl' : '_submit',
   150 			'keydown' : 'keyboardAccessible'
   150 			'keydown' : 'keyboardAccessible'
   151 		},
   151 		},
   152 
   152 
   153 		// Cache current selected widget
   153 		// Cache current selected widget.
   154 		selected: null,
   154 		selected: null,
   155 
   155 
   156 		// Cache sidebar control which has opened panel
   156 		// Cache sidebar control which has opened panel.
   157 		currentSidebarControl: null,
   157 		currentSidebarControl: null,
   158 		$search: null,
   158 		$search: null,
   159 		$clearResults: null,
   159 		$clearResults: null,
   160 		searchMatchesCount: null,
   160 		searchMatchesCount: null,
   161 
   161 
   179 			this.updateList();
   179 			this.updateList();
   180 
   180 
   181 			// Set the initial search count to the number of available widgets.
   181 			// Set the initial search count to the number of available widgets.
   182 			this.searchMatchesCount = this.collection.length;
   182 			this.searchMatchesCount = this.collection.length;
   183 
   183 
   184 			// If the available widgets panel is open and the customize controls are
   184 			/*
   185 			// interacted with (i.e. available widgets panel is blurred) then close the
   185 			 * If the available widgets panel is open and the customize controls
   186 			// available widgets panel. Also close on back button click.
   186 			 * are interacted with (i.e. available widgets panel is blurred) then
       
   187 			 * close the available widgets panel. Also close on back button click.
       
   188 			 */
   187 			$( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
   189 			$( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
   188 				var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
   190 				var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
   189 				if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
   191 				if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
   190 					self.close();
   192 					self.close();
   191 				}
   193 				}
   192 			} );
   194 			} );
   193 
   195 
   194 			// Clear the search results and trigger a `keyup` event to fire a new search.
   196 			// Clear the search results and trigger an `input` event to fire a new search.
   195 			this.$clearResults.on( 'click', function() {
   197 			this.$clearResults.on( 'click', function() {
   196 				self.$search.val( '' ).focus().trigger( 'keyup' );
   198 				self.$search.val( '' ).focus().trigger( 'input' );
   197 			} );
   199 			} );
   198 
   200 
   199 			// Close the panel if the URL in the preview changes
   201 			// Close the panel if the URL in the preview changes.
   200 			api.previewer.bind( 'url', this.close );
   202 			api.previewer.bind( 'url', this.close );
   201 		},
   203 		},
   202 
   204 
   203 		/**
   205 		/**
   204 		 * Performs a search and handles selected widget.
   206 		 * Performs a search and handles selected widget.
   210 			// Update the search matches count.
   212 			// Update the search matches count.
   211 			this.updateSearchMatchesCount();
   213 			this.updateSearchMatchesCount();
   212 			// Announce how many search results.
   214 			// Announce how many search results.
   213 			this.announceSearchMatches();
   215 			this.announceSearchMatches();
   214 
   216 
   215 			// Remove a widget from being selected if it is no longer visible
   217 			// Remove a widget from being selected if it is no longer visible.
   216 			if ( this.selected && ! this.selected.is( ':visible' ) ) {
   218 			if ( this.selected && ! this.selected.is( ':visible' ) ) {
   217 				this.selected.removeClass( 'selected' );
   219 				this.selected.removeClass( 'selected' );
   218 				this.selected = null;
   220 				this.selected = null;
   219 			}
   221 			}
   220 
   222 
   221 			// If a widget was selected but the filter value has been cleared out, clear selection
   223 			// If a widget was selected but the filter value has been cleared out, clear selection.
   222 			if ( this.selected && ! event.target.value ) {
   224 			if ( this.selected && ! event.target.value ) {
   223 				this.selected.removeClass( 'selected' );
   225 				this.selected.removeClass( 'selected' );
   224 				this.selected = null;
   226 				this.selected = null;
   225 			}
   227 			}
   226 
   228 
   227 			// If a filter has been entered and a widget hasn't been selected, select the first one shown
   229 			// If a filter has been entered and a widget hasn't been selected, select the first one shown.
   228 			if ( ! this.selected && event.target.value ) {
   230 			if ( ! this.selected && event.target.value ) {
   229 				firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
   231 				firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
   230 				if ( firstVisible.length ) {
   232 				if ( firstVisible.length ) {
   231 					this.select( firstVisible );
   233 					this.select( firstVisible );
   232 				}
   234 				}
   298 
   300 
   299 		/**
   301 		/**
   300 		 * Handles submit for keypress and click on widget.
   302 		 * Handles submit for keypress and click on widget.
   301 		 */
   303 		 */
   302 		_submit: function( event ) {
   304 		_submit: function( event ) {
   303 			// Only proceed with keypress if it is Enter or Spacebar
   305 			// Only proceed with keypress if it is Enter or Spacebar.
   304 			if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
   306 			if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
   305 				return;
   307 				return;
   306 			}
   308 			}
   307 
   309 
   308 			this.submit( $( event.currentTarget ) );
   310 			this.submit( $( event.currentTarget ) );
   342 		 * Opens the panel.
   344 		 * Opens the panel.
   343 		 */
   345 		 */
   344 		open: function( sidebarControl ) {
   346 		open: function( sidebarControl ) {
   345 			this.currentSidebarControl = sidebarControl;
   347 			this.currentSidebarControl = sidebarControl;
   346 
   348 
   347 			// Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens
   349 			// Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens.
   348 			_( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
   350 			_( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
   349 				if ( control.params.is_wide ) {
   351 				if ( control.params.is_wide ) {
   350 					control.collapseForm();
   352 					control.collapseForm();
   351 				}
   353 				}
   352 			} );
   354 			} );
   357 
   359 
   358 			$( 'body' ).addClass( 'adding-widget' );
   360 			$( 'body' ).addClass( 'adding-widget' );
   359 
   361 
   360 			this.$el.find( '.selected' ).removeClass( 'selected' );
   362 			this.$el.find( '.selected' ).removeClass( 'selected' );
   361 
   363 
   362 			// Reset search
   364 			// Reset search.
   363 			this.collection.doSearch( '' );
   365 			this.collection.doSearch( '' );
   364 
   366 
   365 			if ( ! api.settings.browser.mobile ) {
   367 			if ( ! api.settings.browser.mobile ) {
   366 				this.$search.focus();
   368 				this.$search.focus();
   367 			}
   369 			}
   380 			this.currentSidebarControl = null;
   382 			this.currentSidebarControl = null;
   381 			this.selected = null;
   383 			this.selected = null;
   382 
   384 
   383 			$( 'body' ).removeClass( 'adding-widget' );
   385 			$( 'body' ).removeClass( 'adding-widget' );
   384 
   386 
   385 			this.$search.val( '' );
   387 			this.$search.val( '' ).trigger( 'input' );
   386 		},
   388 		},
   387 
   389 
   388 		/**
   390 		/**
   389 		 * Adds keyboard accessiblity to the panel.
   391 		 * Adds keyboard accessiblity to the panel.
   390 		 */
   392 		 */
   425 				}
   427 				}
   426 
   428 
   427 				return;
   429 				return;
   428 			}
   430 			}
   429 
   431 
   430 			// If enter pressed but nothing entered, don't do anything
   432 			// If enter pressed but nothing entered, don't do anything.
   431 			if ( isEnter && ! this.$search.val() ) {
   433 			if ( isEnter && ! this.$search.val() ) {
   432 				return;
   434 				return;
   433 			}
   435 			}
   434 
   436 
   435 			if ( isEnter ) {
   437 			if ( isEnter ) {
   455 	api.Widgets.formSyncHandlers = {
   457 	api.Widgets.formSyncHandlers = {
   456 
   458 
   457 		/**
   459 		/**
   458 		 * @param {jQuery.Event} e
   460 		 * @param {jQuery.Event} e
   459 		 * @param {jQuery} widget
   461 		 * @param {jQuery} widget
   460 		 * @param {String} newForm
   462 		 * @param {string} newForm
   461 		 */
   463 		 */
   462 		rss: function( e, widget, newForm ) {
   464 		rss: function( e, widget, newForm ) {
   463 			var oldWidgetError = widget.find( '.widget-error:first' ),
   465 			var oldWidgetError = widget.find( '.widget-error:first' ),
   464 				newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
   466 				newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
   465 
   467 
   599 		 * Handle changes to the setting
   601 		 * Handle changes to the setting
   600 		 */
   602 		 */
   601 		_setupModel: function() {
   603 		_setupModel: function() {
   602 			var self = this, rememberSavedWidgetId;
   604 			var self = this, rememberSavedWidgetId;
   603 
   605 
   604 			// Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
   606 			// Remember saved widgets so we know which to trash (move to inactive widgets sidebar).
   605 			rememberSavedWidgetId = function() {
   607 			rememberSavedWidgetId = function() {
   606 				api.Widgets.savedWidgetIds[self.params.widget_id] = true;
   608 				api.Widgets.savedWidgetIds[self.params.widget_id] = true;
   607 			};
   609 			};
   608 			api.bind( 'ready', rememberSavedWidgetId );
   610 			api.bind( 'ready', rememberSavedWidgetId );
   609 			api.bind( 'saved', rememberSavedWidgetId );
   611 			api.bind( 'saved', rememberSavedWidgetId );
   610 
   612 
   611 			this._updateCount = 0;
   613 			this._updateCount = 0;
   612 			this.isWidgetUpdating = false;
   614 			this.isWidgetUpdating = false;
   613 			this.liveUpdateMode = true;
   615 			this.liveUpdateMode = true;
   614 
   616 
   615 			// Update widget whenever model changes
   617 			// Update widget whenever model changes.
   616 			this.setting.bind( function( to, from ) {
   618 			this.setting.bind( function( to, from ) {
   617 				if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
   619 				if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
   618 					self.updateWidget( { instance: to } );
   620 					self.updateWidget( { instance: to } );
   619 				}
   621 				}
   620 			} );
   622 			} );
   655 					windowHeight = $( window ).height(),
   657 					windowHeight = $( window ).height(),
   656 					formHeight = $widgetForm.outerHeight(),
   658 					formHeight = $widgetForm.outerHeight(),
   657 					top;
   659 					top;
   658 				$widgetInside.css( 'max-height', windowHeight );
   660 				$widgetInside.css( 'max-height', windowHeight );
   659 				top = Math.max(
   661 				top = Math.max(
   660 					0, // prevent top from going off screen
   662 					0, // Prevent top from going off screen.
   661 					Math.min(
   663 					Math.min(
   662 						Math.max( offsetTop, 0 ), // distance widget in panel is from top of screen
   664 						Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen.
   663 						windowHeight - formHeight // flush up against bottom of screen
   665 						windowHeight - formHeight // Flush up against bottom of screen.
   664 					)
   666 					)
   665 				);
   667 				);
   666 				$widgetInside.css( 'top', top );
   668 				$widgetInside.css( 'top', top );
   667 			};
   669 			};
   668 
   670 
   677 				$customizeSidebar.off( 'scroll', positionWidget );
   679 				$customizeSidebar.off( 'scroll', positionWidget );
   678 				$( window ).off( 'resize', positionWidget );
   680 				$( window ).off( 'resize', positionWidget );
   679 				$themeControlsContainer.off( 'expanded collapsed', positionWidget );
   681 				$themeControlsContainer.off( 'expanded collapsed', positionWidget );
   680 			} );
   682 			} );
   681 
   683 
   682 			// Reposition whenever a sidebar's widgets are changed
   684 			// Reposition whenever a sidebar's widgets are changed.
   683 			api.each( function( setting ) {
   685 			api.each( function( setting ) {
   684 				if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
   686 				if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
   685 					setting.bind( function() {
   687 					setting.bind( function() {
   686 						if ( self.container.hasClass( 'expanded' ) ) {
   688 						if ( self.container.hasClass( 'expanded' ) ) {
   687 							positionWidget();
   689 							positionWidget();
   708 			} );
   710 			} );
   709 
   711 
   710 			$closeBtn = this.container.find( '.widget-control-close' );
   712 			$closeBtn = this.container.find( '.widget-control-close' );
   711 			$closeBtn.on( 'click', function() {
   713 			$closeBtn.on( 'click', function() {
   712 				self.collapse();
   714 				self.collapse();
   713 				self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
   715 				self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility.
   714 			} );
   716 			} );
   715 		},
   717 		},
   716 
   718 
   717 		/**
   719 		/**
   718 		 * Update the title of the form if a title field is entered
   720 		 * Update the title of the form if a title field is entered
   835 					} else {
   837 					} else {
   836 						self.moveDown();
   838 						self.moveDown();
   837 						wp.a11y.speak( l10n.widgetMovedDown );
   839 						wp.a11y.speak( l10n.widgetMovedDown );
   838 					}
   840 					}
   839 
   841 
   840 					$( this ).focus(); // re-focus after the container was moved
   842 					$( this ).focus(); // Re-focus after the container was moved.
   841 				}
   843 				}
   842 			} );
   844 			} );
   843 
   845 
   844 			/**
   846 			/**
   845 			 * Handle selecting a sidebar to move to
   847 			 * Handle selecting a sidebar to move to
   883 		 * Highlight widgets in preview when interacted with in the Customizer
   885 		 * Highlight widgets in preview when interacted with in the Customizer
   884 		 */
   886 		 */
   885 		_setupHighlightEffects: function() {
   887 		_setupHighlightEffects: function() {
   886 			var self = this;
   888 			var self = this;
   887 
   889 
   888 			// Highlight whenever hovering or clicking over the form
   890 			// Highlight whenever hovering or clicking over the form.
   889 			this.container.on( 'mouseenter click', function() {
   891 			this.container.on( 'mouseenter click', function() {
   890 				self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
   892 				self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
   891 			} );
   893 			} );
   892 
   894 
   893 			// Highlight when the setting is updated
   895 			// Highlight when the setting is updated.
   894 			this.setting.bind( function() {
   896 			this.setting.bind( function() {
   895 				self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
   897 				self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
   896 			} );
   898 			} );
   897 		},
   899 		},
   898 
   900 
   904 				$saveBtn, updateWidgetDebounced, formSyncHandler;
   906 				$saveBtn, updateWidgetDebounced, formSyncHandler;
   905 
   907 
   906 			$widgetRoot = this.container.find( '.widget:first' );
   908 			$widgetRoot = this.container.find( '.widget:first' );
   907 			$widgetContent = $widgetRoot.find( '.widget-content:first' );
   909 			$widgetContent = $widgetRoot.find( '.widget-content:first' );
   908 
   910 
   909 			// Configure update button
   911 			// Configure update button.
   910 			$saveBtn = this.container.find( '.widget-control-save' );
   912 			$saveBtn = this.container.find( '.widget-control-save' );
   911 			$saveBtn.val( l10n.saveBtnLabel );
   913 			$saveBtn.val( l10n.saveBtnLabel );
   912 			$saveBtn.attr( 'title', l10n.saveBtnTooltip );
   914 			$saveBtn.attr( 'title', l10n.saveBtnTooltip );
   913 			$saveBtn.removeClass( 'button-primary' );
   915 			$saveBtn.removeClass( 'button-primary' );
   914 			$saveBtn.on( 'click', function( e ) {
   916 			$saveBtn.on( 'click', function( e ) {
   918 
   920 
   919 			updateWidgetDebounced = _.debounce( function() {
   921 			updateWidgetDebounced = _.debounce( function() {
   920 				self.updateWidget();
   922 				self.updateWidget();
   921 			}, 250 );
   923 			}, 250 );
   922 
   924 
   923 			// Trigger widget form update when hitting Enter within an input
   925 			// Trigger widget form update when hitting Enter within an input.
   924 			$widgetContent.on( 'keydown', 'input', function( e ) {
   926 			$widgetContent.on( 'keydown', 'input', function( e ) {
   925 				if ( 13 === e.which ) { // Enter
   927 				if ( 13 === e.which ) { // Enter.
   926 					e.preventDefault();
   928 					e.preventDefault();
   927 					self.updateWidget( { ignoreActiveElement: true } );
   929 					self.updateWidget( { ignoreActiveElement: true } );
   928 				}
   930 				}
   929 			} );
   931 			} );
   930 
   932 
   931 			// Handle widgets that support live previews
   933 			// Handle widgets that support live previews.
   932 			$widgetContent.on( 'change input propertychange', ':input', function( e ) {
   934 			$widgetContent.on( 'change input propertychange', ':input', function( e ) {
   933 				if ( ! self.liveUpdateMode ) {
   935 				if ( ! self.liveUpdateMode ) {
   934 					return;
   936 					return;
   935 				}
   937 				}
   936 				if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
   938 				if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
   937 					updateWidgetDebounced();
   939 					updateWidgetDebounced();
   938 				}
   940 				}
   939 			} );
   941 			} );
   940 
   942 
   941 			// Remove loading indicators when the setting is saved and the preview updates
   943 			// Remove loading indicators when the setting is saved and the preview updates.
   942 			this.setting.previewer.channel.bind( 'synced', function() {
   944 			this.setting.previewer.channel.bind( 'synced', function() {
   943 				self.container.removeClass( 'previewer-loading' );
   945 				self.container.removeClass( 'previewer-loading' );
   944 			} );
   946 			} );
   945 
   947 
   946 			api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
   948 			api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
   964 		 *
   966 		 *
   965 		 * Overrides api.Control.toggle()
   967 		 * Overrides api.Control.toggle()
   966 		 *
   968 		 *
   967 		 * @since 4.1.0
   969 		 * @since 4.1.0
   968 		 *
   970 		 *
   969 		 * @param {Boolean}   active
   971 		 * @param {boolean}   active
   970 		 * @param {Object}    args
   972 		 * @param {Object}    args
   971 		 * @param {function}  args.completeCallback
   973 		 * @param {function}  args.completeCallback
   972 		 */
   974 		 */
   973 		onChangeActive: function ( active, args ) {
   975 		onChangeActive: function ( active, args ) {
   974 			// Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
   976 			// Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments.
   975 			this.container.toggleClass( 'widget-rendered', active );
   977 			this.container.toggleClass( 'widget-rendered', active );
   976 			if ( args.completeCallback ) {
   978 			if ( args.completeCallback ) {
   977 				args.completeCallback();
   979 				args.completeCallback();
   978 			}
   980 			}
   979 		},
   981 		},
   982 		 * Set up event handlers for widget removal
   984 		 * Set up event handlers for widget removal
   983 		 */
   985 		 */
   984 		_setupRemoveUI: function() {
   986 		_setupRemoveUI: function() {
   985 			var self = this, $removeBtn, replaceDeleteWithRemove;
   987 			var self = this, $removeBtn, replaceDeleteWithRemove;
   986 
   988 
   987 			// Configure remove button
   989 			// Configure remove button.
   988 			$removeBtn = this.container.find( '.widget-control-remove' );
   990 			$removeBtn = this.container.find( '.widget-control-remove' );
   989 			$removeBtn.on( 'click', function() {
   991 			$removeBtn.on( 'click', function() {
   990 				// Find an adjacent element to add focus to when this widget goes away
   992 				// Find an adjacent element to add focus to when this widget goes away.
   991 				var $adjacentFocusTarget;
   993 				var $adjacentFocusTarget;
   992 				if ( self.container.next().is( '.customize-control-widget_form' ) ) {
   994 				if ( self.container.next().is( '.customize-control-widget_form' ) ) {
   993 					$adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
   995 					$adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
   994 				} else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
   996 				} else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
   995 					$adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
   997 					$adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
  1012 					}
  1014 					}
  1013 
  1015 
  1014 					sidebarWidgetIds.splice( i, 1 );
  1016 					sidebarWidgetIds.splice( i, 1 );
  1015 					sidebarsWidgetsControl.setting( sidebarWidgetIds );
  1017 					sidebarsWidgetsControl.setting( sidebarWidgetIds );
  1016 
  1018 
  1017 					$adjacentFocusTarget.focus(); // keyboard accessibility
  1019 					$adjacentFocusTarget.focus(); // Keyboard accessibility.
  1018 				} );
  1020 				} );
  1019 			} );
  1021 			} );
  1020 
  1022 
  1021 			replaceDeleteWithRemove = function() {
  1023 			replaceDeleteWithRemove = function() {
  1022 				$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete"
  1024 				$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete".
  1023 				$removeBtn.attr( 'title', l10n.removeBtnTooltip );
  1025 				$removeBtn.attr( 'title', l10n.removeBtnTooltip );
  1024 			};
  1026 			};
  1025 
  1027 
  1026 			if ( this.params.is_new ) {
  1028 			if ( this.params.is_new ) {
  1027 				api.bind( 'saved', replaceDeleteWithRemove );
  1029 				api.bind( 'saved', replaceDeleteWithRemove );
  1036 		 * be aligned to copy the sanitized over. The elements returned by this
  1038 		 * be aligned to copy the sanitized over. The elements returned by this
  1037 		 * are passed into this._getInputsSignature(), and they are iterated
  1039 		 * are passed into this._getInputsSignature(), and they are iterated
  1038 		 * over when copying sanitized values over to the form loaded.
  1040 		 * over when copying sanitized values over to the form loaded.
  1039 		 *
  1041 		 *
  1040 		 * @param {jQuery} container element in which to look for inputs
  1042 		 * @param {jQuery} container element in which to look for inputs
  1041 		 * @returns {jQuery} inputs
  1043 		 * @return {jQuery} inputs
  1042 		 * @private
  1044 		 * @private
  1043 		 */
  1045 		 */
  1044 		_getInputs: function( container ) {
  1046 		_getInputs: function( container ) {
  1045 			return $( container ).find( ':input[name]' );
  1047 			return $( container ).find( ':input[name]' );
  1046 		},
  1048 		},
  1048 		/**
  1050 		/**
  1049 		 * Iterate over supplied inputs and create a signature string for all of them together.
  1051 		 * Iterate over supplied inputs and create a signature string for all of them together.
  1050 		 * This string can be used to compare whether or not the form has all of the same fields.
  1052 		 * This string can be used to compare whether or not the form has all of the same fields.
  1051 		 *
  1053 		 *
  1052 		 * @param {jQuery} inputs
  1054 		 * @param {jQuery} inputs
  1053 		 * @returns {string}
  1055 		 * @return {string}
  1054 		 * @private
  1056 		 * @private
  1055 		 */
  1057 		 */
  1056 		_getInputsSignature: function( inputs ) {
  1058 		_getInputsSignature: function( inputs ) {
  1057 			var inputsSignatures = _( inputs ).map( function( input ) {
  1059 			var inputsSignatures = _( inputs ).map( function( input ) {
  1058 				var $input = $( input ), signatureParts;
  1060 				var $input = $( input ), signatureParts;
  1071 
  1073 
  1072 		/**
  1074 		/**
  1073 		 * Get the state for an input depending on its type.
  1075 		 * Get the state for an input depending on its type.
  1074 		 *
  1076 		 *
  1075 		 * @param {jQuery|Element} input
  1077 		 * @param {jQuery|Element} input
  1076 		 * @returns {string|boolean|array|*}
  1078 		 * @return {string|boolean|Array|*}
  1077 		 * @private
  1079 		 * @private
  1078 		 */
  1080 		 */
  1079 		_getInputState: function( input ) {
  1081 		_getInputState: function( input ) {
  1080 			input = $( input );
  1082 			input = $( input );
  1081 			if ( input.is( ':radio, :checkbox' ) ) {
  1083 			if ( input.is( ':radio, :checkbox' ) ) {
  1091 
  1093 
  1092 		/**
  1094 		/**
  1093 		 * Update an input's state based on its type.
  1095 		 * Update an input's state based on its type.
  1094 		 *
  1096 		 *
  1095 		 * @param {jQuery|Element} input
  1097 		 * @param {jQuery|Element} input
  1096 		 * @param {string|boolean|array|*} state
  1098 		 * @param {string|boolean|Array|*} state
  1097 		 * @private
  1099 		 * @private
  1098 		 */
  1100 		 */
  1099 		_setInputState: function ( input, state ) {
  1101 		_setInputState: function ( input, state ) {
  1100 			input = $( input );
  1102 			input = $( input );
  1101 			if ( input.is( ':radio, :checkbox' ) ) {
  1103 			if ( input.is( ':radio, :checkbox' ) ) {
  1102 				input.prop( 'checked', state );
  1104 				input.prop( 'checked', state );
  1103 			} else if ( input.is( 'select[multiple]' ) ) {
  1105 			} else if ( input.is( 'select[multiple]' ) ) {
  1104 				if ( ! $.isArray( state ) ) {
  1106 				if ( ! $.isArray( state ) ) {
  1105 					state = [];
  1107 					state = [];
  1106 				} else {
  1108 				} else {
  1107 					// Make sure all state items are strings since the DOM value is a string
  1109 					// Make sure all state items are strings since the DOM value is a string.
  1108 					state = _.map( state, function ( value ) {
  1110 					state = _.map( state, function ( value ) {
  1109 						return String( value );
  1111 						return String( value );
  1110 					} );
  1112 					} );
  1111 				}
  1113 				}
  1112 				input.find( 'option' ).each( function () {
  1114 				input.find( 'option' ).each( function () {
  1139 
  1141 
  1140 		/**
  1142 		/**
  1141 		 * Submit the widget form via Ajax and get back the updated instance,
  1143 		 * Submit the widget form via Ajax and get back the updated instance,
  1142 		 * along with the new widget control form to render.
  1144 		 * along with the new widget control form to render.
  1143 		 *
  1145 		 *
  1144 		 * @param {object} [args]
  1146 		 * @param {Object} [args]
  1145 		 * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
  1147 		 * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
  1146 		 * @param {Function|null} [args.complete=null]  Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
  1148 		 * @param {Function|null} [args.complete=null]  Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
  1147 		 * @param {Boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
  1149 		 * @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
  1148 		 */
  1150 		 */
  1149 		updateWidget: function( args ) {
  1151 		updateWidget: function( args ) {
  1150 			var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
  1152 			var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
  1151 				updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
  1153 				updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
  1152 
  1154 
  1166 			updateNumber = this._updateCount;
  1168 			updateNumber = this._updateCount;
  1167 
  1169 
  1168 			$widgetRoot = this.container.find( '.widget:first' );
  1170 			$widgetRoot = this.container.find( '.widget:first' );
  1169 			$widgetContent = $widgetRoot.find( '.widget-content:first' );
  1171 			$widgetContent = $widgetRoot.find( '.widget-content:first' );
  1170 
  1172 
  1171 			// Remove a previous error message
  1173 			// Remove a previous error message.
  1172 			$widgetContent.find( '.widget-error' ).remove();
  1174 			$widgetContent.find( '.widget-error' ).remove();
  1173 
  1175 
  1174 			this.container.addClass( 'widget-form-loading' );
  1176 			this.container.addClass( 'widget-form-loading' );
  1175 			this.container.addClass( 'previewer-loading' );
  1177 			this.container.addClass( 'previewer-loading' );
  1176 			processing = api.state( 'processing' );
  1178 			processing = api.state( 'processing' );
  1188 			params.customized = wp.customize.previewer.query().customized;
  1190 			params.customized = wp.customize.previewer.query().customized;
  1189 
  1191 
  1190 			data = $.param( params );
  1192 			data = $.param( params );
  1191 			$inputs = this._getInputs( $widgetContent );
  1193 			$inputs = this._getInputs( $widgetContent );
  1192 
  1194 
  1193 			// Store the value we're submitting in data so that when the response comes back,
  1195 			/*
  1194 			// we know if it got sanitized; if there is no difference in the sanitized value,
  1196 			 * Store the value we're submitting in data so that when the response comes back,
  1195 			// then we do not need to touch the UI and mess up the user's ongoing editing.
  1197 			 * we know if it got sanitized; if there is no difference in the sanitized value,
       
  1198 			 * then we do not need to touch the UI and mess up the user's ongoing editing.
       
  1199 			 */
  1196 			$inputs.each( function() {
  1200 			$inputs.each( function() {
  1197 				$( this ).data( 'state' + updateNumber, self._getInputState( this ) );
  1201 				$( this ).data( 'state' + updateNumber, self._getInputState( this ) );
  1198 			} );
  1202 			} );
  1199 
  1203 
  1200 			if ( instanceOverride ) {
  1204 			if ( instanceOverride ) {
  1233 				if ( r.success ) {
  1237 				if ( r.success ) {
  1234 					sanitizedForm = $( '<div>' + r.data.form + '</div>' );
  1238 					sanitizedForm = $( '<div>' + r.data.form + '</div>' );
  1235 					$sanitizedInputs = self._getInputs( sanitizedForm );
  1239 					$sanitizedInputs = self._getInputs( sanitizedForm );
  1236 					hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
  1240 					hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
  1237 
  1241 
  1238 					// Restore live update mode if sanitized fields are now aligned with the existing fields
  1242 					// Restore live update mode if sanitized fields are now aligned with the existing fields.
  1239 					if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
  1243 					if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
  1240 						self.liveUpdateMode = true;
  1244 						self.liveUpdateMode = true;
  1241 						self.container.removeClass( 'widget-form-disabled' );
  1245 						self.container.removeClass( 'widget-form-disabled' );
  1242 						self.container.find( 'input[name="savewidget"]' ).hide();
  1246 						self.container.find( 'input[name="savewidget"]' ).hide();
  1243 					}
  1247 					}
  1244 
  1248 
  1245 					// Sync sanitized field states to existing fields if they are aligned
  1249 					// Sync sanitized field states to existing fields if they are aligned.
  1246 					if ( hasSameInputsInResponse && self.liveUpdateMode ) {
  1250 					if ( hasSameInputsInResponse && self.liveUpdateMode ) {
  1247 						$inputs.each( function( i ) {
  1251 						$inputs.each( function( i ) {
  1248 							var $input = $( this ),
  1252 							var $input = $( this ),
  1249 								$sanitizedInput = $( $sanitizedInputs[i] ),
  1253 								$sanitizedInput = $( $sanitizedInputs[i] ),
  1250 								submittedState, sanitizedState,	canUpdateState;
  1254 								submittedState, sanitizedState,	canUpdateState;
  1259 							}
  1263 							}
  1260 						} );
  1264 						} );
  1261 
  1265 
  1262 						$( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
  1266 						$( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
  1263 
  1267 
  1264 					// Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled
  1268 					// Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled.
  1265 					} else if ( self.liveUpdateMode ) {
  1269 					} else if ( self.liveUpdateMode ) {
  1266 						self.liveUpdateMode = false;
  1270 						self.liveUpdateMode = false;
  1267 						self.container.find( 'input[name="savewidget"]' ).show();
  1271 						self.container.find( 'input[name="savewidget"]' ).show();
  1268 						isLiveUpdateAborted = true;
  1272 						isLiveUpdateAborted = true;
  1269 
  1273 
  1270 					// Otherwise, replace existing form with the sanitized form
  1274 					// Otherwise, replace existing form with the sanitized form.
  1271 					} else {
  1275 					} else {
  1272 						$widgetContent.html( r.data.form );
  1276 						$widgetContent.html( r.data.form );
  1273 
  1277 
  1274 						self.container.removeClass( 'widget-form-disabled' );
  1278 						self.container.removeClass( 'widget-form-disabled' );
  1275 
  1279 
  1281 					 * needing to be rendered, and so we can preempt the event for the
  1285 					 * needing to be rendered, and so we can preempt the event for the
  1282 					 * preview finishing loading.
  1286 					 * preview finishing loading.
  1283 					 */
  1287 					 */
  1284 					isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
  1288 					isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
  1285 					if ( isChanged ) {
  1289 					if ( isChanged ) {
  1286 						self.isWidgetUpdating = true; // suppress triggering another updateWidget
  1290 						self.isWidgetUpdating = true; // Suppress triggering another updateWidget.
  1287 						self.setting( r.data.instance );
  1291 						self.setting( r.data.instance );
  1288 						self.isWidgetUpdating = false;
  1292 						self.isWidgetUpdating = false;
  1289 					} else {
  1293 					} else {
  1290 						// no change was made, so stop the spinner now instead of when the preview would updates
  1294 						// No change was made, so stop the spinner now instead of when the preview would updates.
  1291 						self.container.removeClass( 'previewer-loading' );
  1295 						self.container.removeClass( 'previewer-loading' );
  1292 					}
  1296 					}
  1293 
  1297 
  1294 					if ( completeCallback ) {
  1298 					if ( completeCallback ) {
  1295 						completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
  1299 						completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
  1296 					}
  1300 					}
  1297 				} else {
  1301 				} else {
  1298 					// General error message
  1302 					// General error message.
  1299 					message = l10n.error;
  1303 					message = l10n.error;
  1300 
  1304 
  1301 					if ( r.data && r.data.message ) {
  1305 					if ( r.data && r.data.message ) {
  1302 						message = r.data.message;
  1306 						message = r.data.message;
  1303 					}
  1307 					}
  1337 		/**
  1341 		/**
  1338 		 * @since 4.1.0
  1342 		 * @since 4.1.0
  1339 		 *
  1343 		 *
  1340 		 * @param {Boolean} expanded
  1344 		 * @param {Boolean} expanded
  1341 		 * @param {Object} [params]
  1345 		 * @param {Object} [params]
  1342 		 * @returns {Boolean} false if state already applied
  1346 		 * @return {Boolean} False if state already applied.
  1343 		 */
  1347 		 */
  1344 		_toggleExpanded: api.Section.prototype._toggleExpanded,
  1348 		_toggleExpanded: api.Section.prototype._toggleExpanded,
  1345 
  1349 
  1346 		/**
  1350 		/**
  1347 		 * @since 4.1.0
  1351 		 * @since 4.1.0
  1348 		 *
  1352 		 *
  1349 		 * @param {Object} [params]
  1353 		 * @param {Object} [params]
  1350 		 * @returns {Boolean} false if already expanded
  1354 		 * @return {Boolean} False if already expanded.
  1351 		 */
  1355 		 */
  1352 		expand: api.Section.prototype.expand,
  1356 		expand: api.Section.prototype.expand,
  1353 
  1357 
  1354 		/**
  1358 		/**
  1355 		 * Expand the widget form control
  1359 		 * Expand the widget form control
  1362 
  1366 
  1363 		/**
  1367 		/**
  1364 		 * @since 4.1.0
  1368 		 * @since 4.1.0
  1365 		 *
  1369 		 *
  1366 		 * @param {Object} [params]
  1370 		 * @param {Object} [params]
  1367 		 * @returns {Boolean} false if already collapsed
  1371 		 * @return {Boolean} False if already collapsed.
  1368 		 */
  1372 		 */
  1369 		collapse: api.Section.prototype.collapse,
  1373 		collapse: api.Section.prototype.collapse,
  1370 
  1374 
  1371 		/**
  1375 		/**
  1372 		 * Collapse the widget form control
  1376 		 * Collapse the widget form control
  1392 		},
  1396 		},
  1393 
  1397 
  1394 		/**
  1398 		/**
  1395 		 * Respond to change in the expanded state.
  1399 		 * Respond to change in the expanded state.
  1396 		 *
  1400 		 *
  1397 		 * @param {Boolean} expanded
  1401 		 * @param {boolean} expanded
  1398 		 * @param {Object} args  merged on top of this.defaultActiveArguments
  1402 		 * @param {Object} args  merged on top of this.defaultActiveArguments
  1399 		 */
  1403 		 */
  1400 		onChangeExpanded: function ( expanded, args ) {
  1404 		onChangeExpanded: function ( expanded, args ) {
  1401 			var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
  1405 			var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
  1402 
  1406 
  1403 			self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
  1407 			self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
  1404 			if ( expanded ) {
  1408 			if ( expanded ) {
  1405 				self.embedWidgetContent();
  1409 				self.embedWidgetContent();
  1406 			}
  1410 			}
  1407 
  1411 
  1408 			// If the expanded state is unchanged only manipulate container expanded states
  1412 			// If the expanded state is unchanged only manipulate container expanded states.
  1409 			if ( args.unchanged ) {
  1413 			if ( args.unchanged ) {
  1410 				if ( expanded ) {
  1414 				if ( expanded ) {
  1411 					api.Control.prototype.expand.call( self, {
  1415 					api.Control.prototype.expand.call( self, {
  1412 						completeCallback:  args.completeCallback
  1416 						completeCallback:  args.completeCallback
  1413 					});
  1417 					});
  1419 			$inside = $widget.find( '.widget-inside:first' );
  1423 			$inside = $widget.find( '.widget-inside:first' );
  1420 			$toggleBtn = this.container.find( '.widget-top button.widget-action' );
  1424 			$toggleBtn = this.container.find( '.widget-top button.widget-action' );
  1421 
  1425 
  1422 			expandControl = function() {
  1426 			expandControl = function() {
  1423 
  1427 
  1424 				// Close all other widget controls before expanding this one
  1428 				// Close all other widget controls before expanding this one.
  1425 				api.control.each( function( otherControl ) {
  1429 				api.control.each( function( otherControl ) {
  1426 					if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1430 					if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1427 						otherControl.collapse();
  1431 						otherControl.collapse();
  1428 					}
  1432 					}
  1429 				} );
  1433 				} );
  1492 		},
  1496 		},
  1493 
  1497 
  1494 		/**
  1498 		/**
  1495 		 * Get the position (index) of the widget in the containing sidebar
  1499 		 * Get the position (index) of the widget in the containing sidebar
  1496 		 *
  1500 		 *
  1497 		 * @returns {Number}
  1501 		 * @return {number}
  1498 		 */
  1502 		 */
  1499 		getWidgetSidebarPosition: function() {
  1503 		getWidgetSidebarPosition: function() {
  1500 			var sidebarWidgetIds, position;
  1504 			var sidebarWidgetIds, position;
  1501 
  1505 
  1502 			sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
  1506 			sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
  1524 		},
  1528 		},
  1525 
  1529 
  1526 		/**
  1530 		/**
  1527 		 * @private
  1531 		 * @private
  1528 		 *
  1532 		 *
  1529 		 * @param {Number} offset 1|-1
  1533 		 * @param {number} offset 1|-1
  1530 		 */
  1534 		 */
  1531 		_moveWidgetByOne: function( offset ) {
  1535 		_moveWidgetByOne: function( offset ) {
  1532 			var i, sidebarWidgetsSetting, sidebarWidgetIds,	adjacentWidgetId;
  1536 			var i, sidebarWidgetsSetting, sidebarWidgetIds,	adjacentWidgetId;
  1533 
  1537 
  1534 			i = this.getWidgetSidebarPosition();
  1538 			i = this.getWidgetSidebarPosition();
  1535 
  1539 
  1536 			sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
  1540 			sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
  1537 			sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // clone
  1541 			sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone.
  1538 			adjacentWidgetId = sidebarWidgetIds[i + offset];
  1542 			adjacentWidgetId = sidebarWidgetIds[i + offset];
  1539 			sidebarWidgetIds[i + offset] = this.params.widget_id;
  1543 			sidebarWidgetIds[i + offset] = this.params.widget_id;
  1540 			sidebarWidgetIds[i] = adjacentWidgetId;
  1544 			sidebarWidgetIds[i] = adjacentWidgetId;
  1541 
  1545 
  1542 			sidebarWidgetsSetting( sidebarWidgetIds );
  1546 			sidebarWidgetsSetting( sidebarWidgetIds );
  1543 		},
  1547 		},
  1544 
  1548 
  1545 		/**
  1549 		/**
  1546 		 * Toggle visibility of the widget move area
  1550 		 * Toggle visibility of the widget move area
  1547 		 *
  1551 		 *
  1548 		 * @param {Boolean} [showOrHide]
  1552 		 * @param {boolean} [showOrHide]
  1549 		 */
  1553 		 */
  1550 		toggleWidgetMoveArea: function( showOrHide ) {
  1554 		toggleWidgetMoveArea: function( showOrHide ) {
  1551 			var self = this, $moveWidgetArea;
  1555 			var self = this, $moveWidgetArea;
  1552 
  1556 
  1553 			$moveWidgetArea = this.container.find( '.move-widget-area' );
  1557 			$moveWidgetArea = this.container.find( '.move-widget-area' );
  1555 			if ( typeof showOrHide === 'undefined' ) {
  1559 			if ( typeof showOrHide === 'undefined' ) {
  1556 				showOrHide = ! $moveWidgetArea.hasClass( 'active' );
  1560 				showOrHide = ! $moveWidgetArea.hasClass( 'active' );
  1557 			}
  1561 			}
  1558 
  1562 
  1559 			if ( showOrHide ) {
  1563 			if ( showOrHide ) {
  1560 				// reset the selected sidebar
  1564 				// Reset the selected sidebar.
  1561 				$moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
  1565 				$moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
  1562 
  1566 
  1563 				$moveWidgetArea.find( 'li' ).filter( function() {
  1567 				$moveWidgetArea.find( 'li' ).filter( function() {
  1564 					return $( this ).data( 'id' ) === self.params.sidebar_id;
  1568 					return $( this ).data( 'id' ) === self.params.sidebar_id;
  1565 				} ).addClass( 'selected' );
  1569 				} ).addClass( 'selected' );
  1649 				};
  1653 				};
  1650 
  1654 
  1651 				/**
  1655 				/**
  1652 				 * Update the notice.
  1656 				 * Update the notice.
  1653 				 *
  1657 				 *
  1654 				 * @returns {void}
  1658 				 * @return {void}
  1655 				 */
  1659 				 */
  1656 				updateNotice = function() {
  1660 				updateNotice = function() {
  1657 					var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
  1661 					var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
  1658 					noticeContainer.empty();
  1662 					noticeContainer.empty();
  1659 
  1663 
  1705 		 * This ensures that the widgets panel appears even when there are no
  1709 		 * This ensures that the widgets panel appears even when there are no
  1706 		 * sidebars displayed on the URL currently being previewed.
  1710 		 * sidebars displayed on the URL currently being previewed.
  1707 		 *
  1711 		 *
  1708 		 * @since 4.4.0
  1712 		 * @since 4.4.0
  1709 		 *
  1713 		 *
  1710 		 * @returns {boolean}
  1714 		 * @return {boolean}
  1711 		 */
  1715 		 */
  1712 		isContextuallyActive: function() {
  1716 		isContextuallyActive: function() {
  1713 			var panel = this;
  1717 			var panel = this;
  1714 			return panel.active();
  1718 			return panel.active();
  1715 		}
  1719 		}
  1778 			this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
  1782 			this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
  1779 				var widgetFormControls, removedWidgetIds, priority;
  1783 				var widgetFormControls, removedWidgetIds, priority;
  1780 
  1784 
  1781 				removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
  1785 				removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
  1782 
  1786 
  1783 				// Filter out any persistent widget IDs for widgets which have been deactivated
  1787 				// Filter out any persistent widget IDs for widgets which have been deactivated.
  1784 				newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) {
  1788 				newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) {
  1785 					var parsedWidgetId = parseWidgetId( newWidgetId );
  1789 					var parsedWidgetId = parseWidgetId( newWidgetId );
  1786 
  1790 
  1787 					return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
  1791 					return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
  1788 				} );
  1792 				} );
  1795 					}
  1799 					}
  1796 
  1800 
  1797 					return widgetFormControl;
  1801 					return widgetFormControl;
  1798 				} );
  1802 				} );
  1799 
  1803 
  1800 				// Sort widget controls to their new positions
  1804 				// Sort widget controls to their new positions.
  1801 				widgetFormControls.sort( function( a, b ) {
  1805 				widgetFormControls.sort( function( a, b ) {
  1802 					var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
  1806 					var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
  1803 						bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
  1807 						bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
  1804 					return aIndex - bIndex;
  1808 					return aIndex - bIndex;
  1805 				});
  1809 				});
  1808 				_( widgetFormControls ).each( function ( control ) {
  1812 				_( widgetFormControls ).each( function ( control ) {
  1809 					control.priority( priority );
  1813 					control.priority( priority );
  1810 					control.section( self.section() );
  1814 					control.section( self.section() );
  1811 					priority += 1;
  1815 					priority += 1;
  1812 				});
  1816 				});
  1813 				self.priority( priority ); // Make sure sidebar control remains at end
  1817 				self.priority( priority ); // Make sure sidebar control remains at end.
  1814 
  1818 
  1815 				// Re-sort widget form controls (including widgets form other sidebars newly moved here)
  1819 				// Re-sort widget form controls (including widgets form other sidebars newly moved here).
  1816 				self._applyCardinalOrderClassNames();
  1820 				self._applyCardinalOrderClassNames();
  1817 
  1821 
  1818 				// If the widget was dragged into the sidebar, make sure the sidebar_id param is updated
  1822 				// If the widget was dragged into the sidebar, make sure the sidebar_id param is updated.
  1819 				_( widgetFormControls ).each( function( widgetFormControl ) {
  1823 				_( widgetFormControls ).each( function( widgetFormControl ) {
  1820 					widgetFormControl.params.sidebar_id = self.params.sidebar_id;
  1824 					widgetFormControl.params.sidebar_id = self.params.sidebar_id;
  1821 				} );
  1825 				} );
  1822 
  1826 
  1823 				// Cleanup after widget removal
  1827 				// Cleanup after widget removal.
  1824 				_( removedWidgetIds ).each( function( removedWidgetId ) {
  1828 				_( removedWidgetIds ).each( function( removedWidgetId ) {
  1825 
  1829 
  1826 					// Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update
  1830 					// Using setTimeout so that when moving a widget to another sidebar,
       
  1831 					// the other sidebars_widgets settings get a chance to update.
  1827 					setTimeout( function() {
  1832 					setTimeout( function() {
  1828 						var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase,
  1833 						var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase,
  1829 							widget, isPresentInAnotherSidebar = false;
  1834 							widget, isPresentInAnotherSidebar = false;
  1830 
  1835 
  1831 						// Check if the widget is in another sidebar
  1836 						// Check if the widget is in another sidebar.
  1832 						api.each( function( otherSetting ) {
  1837 						api.each( function( otherSetting ) {
  1833 							if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
  1838 							if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
  1834 								return;
  1839 								return;
  1835 							}
  1840 							}
  1836 
  1841 
  1847 							return;
  1852 							return;
  1848 						}
  1853 						}
  1849 
  1854 
  1850 						removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
  1855 						removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
  1851 
  1856 
  1852 						// Detect if widget control was dragged to another sidebar
  1857 						// Detect if widget control was dragged to another sidebar.
  1853 						wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
  1858 						wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
  1854 
  1859 
  1855 						// Delete any widget form controls for removed widgets
  1860 						// Delete any widget form controls for removed widgets.
  1856 						if ( removedControl && ! wasDraggedToAnotherSidebar ) {
  1861 						if ( removedControl && ! wasDraggedToAnotherSidebar ) {
  1857 							api.control.remove( removedControl.id );
  1862 							api.control.remove( removedControl.id );
  1858 							removedControl.container.remove();
  1863 							removedControl.container.remove();
  1859 						}
  1864 						}
  1860 
  1865 
  1861 						// Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
  1866 						// Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved.
  1862 						// This prevents the inactive widgets sidebar from overflowing with throwaway widgets
  1867 						// This prevents the inactive widgets sidebar from overflowing with throwaway widgets.
  1863 						if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
  1868 						if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
  1864 							inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
  1869 							inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
  1865 							inactiveWidgets.push( removedWidgetId );
  1870 							inactiveWidgets.push( removedWidgetId );
  1866 							api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
  1871 							api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
  1867 						}
  1872 						}
  1868 
  1873 
  1869 						// Make old single widget available for adding again
  1874 						// Make old single widget available for adding again.
  1870 						removedIdBase = parseWidgetId( removedWidgetId ).id_base;
  1875 						removedIdBase = parseWidgetId( removedWidgetId ).id_base;
  1871 						widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } );
  1876 						widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } );
  1872 						if ( widget && ! widget.get( 'is_multi' ) ) {
  1877 						if ( widget && ! widget.get( 'is_multi' ) ) {
  1873 							widget.set( 'is_disabled', false );
  1878 							widget.set( 'is_disabled', false );
  1874 						}
  1879 						}
  1913 			this.$controlSection.find( '.accordion-section-title' ).droppable({
  1918 			this.$controlSection.find( '.accordion-section-title' ).droppable({
  1914 				accept: '.customize-control-widget_form',
  1919 				accept: '.customize-control-widget_form',
  1915 				over: function() {
  1920 				over: function() {
  1916 					var section = api.section( self.section.get() );
  1921 					var section = api.section( self.section.get() );
  1917 					section.expand({
  1922 					section.expand({
  1918 						allowMultiple: true, // Prevent the section being dragged from to be collapsed
  1923 						allowMultiple: true, // Prevent the section being dragged from to be collapsed.
  1919 						completeCallback: function () {
  1924 						completeCallback: function () {
  1920 							// @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed
  1925 							// @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed.
  1921 							api.section.each( function ( otherSection ) {
  1926 							api.section.each( function ( otherSection ) {
  1922 								if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
  1927 								if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
  1923 									otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
  1928 									otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
  1924 								}
  1929 								}
  1925 							} );
  1930 							} );
  2000 		 **********************************************************************/
  2005 		 **********************************************************************/
  2001 
  2006 
  2002 		/**
  2007 		/**
  2003 		 * Enable/disable the reordering UI
  2008 		 * Enable/disable the reordering UI
  2004 		 *
  2009 		 *
  2005 		 * @param {Boolean} showOrHide to enable/disable reordering
  2010 		 * @param {boolean} showOrHide to enable/disable reordering
  2006 		 *
  2011 		 *
  2007 		 * @todo We should have a reordering state instead and rename this to onChangeReordering
  2012 		 * @todo We should have a reordering state instead and rename this to onChangeReordering
  2008 		 */
  2013 		 */
  2009 		toggleReordering: function( showOrHide ) {
  2014 		toggleReordering: function( showOrHide ) {
  2010 			var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
  2015 			var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
  2057 
  2062 
  2058 			return formControls;
  2063 			return formControls;
  2059 		},
  2064 		},
  2060 
  2065 
  2061 		/**
  2066 		/**
  2062 		 * @param {string} widgetId or an id_base for adding a previously non-existing widget
  2067 		 * @param {string} widgetId or an id_base for adding a previously non-existing widget.
  2063 		 * @returns {object|false} widget_form control instance, or false on error
  2068 		 * @return {Object|false} widget_form control instance, or false on error.
  2064 		 */
  2069 		 */
  2065 		addWidget: function( widgetId ) {
  2070 		addWidget: function( widgetId ) {
  2066 			var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
  2071 			var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
  2067 				parsedWidgetId = parseWidgetId( widgetId ),
  2072 				parsedWidgetId = parseWidgetId( widgetId ),
  2068 				widgetNumber = parsedWidgetId.number,
  2073 				widgetNumber = parsedWidgetId.number,
  2076 
  2081 
  2077 			if ( widgetNumber && ! widget.get( 'is_multi' ) ) {
  2082 			if ( widgetNumber && ! widget.get( 'is_multi' ) ) {
  2078 				return false;
  2083 				return false;
  2079 			}
  2084 			}
  2080 
  2085 
  2081 			// Set up new multi widget
  2086 			// Set up new multi widget.
  2082 			if ( widget.get( 'is_multi' ) && ! widgetNumber ) {
  2087 			if ( widget.get( 'is_multi' ) && ! widgetNumber ) {
  2083 				widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
  2088 				widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
  2084 				widgetNumber = widget.get( 'multi_number' );
  2089 				widgetNumber = widget.get( 'multi_number' );
  2085 			}
  2090 			}
  2086 
  2091 
  2088 			if ( widget.get( 'is_multi' ) ) {
  2093 			if ( widget.get( 'is_multi' ) ) {
  2089 				controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) {
  2094 				controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) {
  2090 					return m.replace( /__i__|%i%/g, widgetNumber );
  2095 					return m.replace( /__i__|%i%/g, widgetNumber );
  2091 				} );
  2096 				} );
  2092 			} else {
  2097 			} else {
  2093 				widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
  2098 				widget.set( 'is_disabled', true ); // Prevent single widget from being added again now.
  2094 			}
  2099 			}
  2095 
  2100 
  2096 			$widget = $( controlHtml );
  2101 			$widget = $( controlHtml );
  2097 
  2102 
  2098 			controlContainer = $( '<li/>' )
  2103 			controlContainer = $( '<li/>' )
  2099 				.addClass( 'customize-control' )
  2104 				.addClass( 'customize-control' )
  2100 				.addClass( 'customize-control-' + controlType )
  2105 				.addClass( 'customize-control-' + controlType )
  2101 				.append( $widget );
  2106 				.append( $widget );
  2102 
  2107 
  2103 			// Remove icon which is visible inside the panel
  2108 			// Remove icon which is visible inside the panel.
  2104 			controlContainer.find( '> .widget-icon' ).remove();
  2109 			controlContainer.find( '> .widget-icon' ).remove();
  2105 
  2110 
  2106 			if ( widget.get( 'is_multi' ) ) {
  2111 			if ( widget.get( 'is_multi' ) ) {
  2107 				controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
  2112 				controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
  2108 				controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
  2113 				controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
  2109 			}
  2114 			}
  2110 
  2115 
  2111 			widgetId = controlContainer.find( '[name="widget-id"]' ).val();
  2116 			widgetId = controlContainer.find( '[name="widget-id"]' ).val();
  2112 
  2117 
  2113 			controlContainer.hide(); // to be slid-down below
  2118 			controlContainer.hide(); // To be slid-down below.
  2114 
  2119 
  2115 			settingId = 'widget_' + widget.get( 'id_base' );
  2120 			settingId = 'widget_' + widget.get( 'id_base' );
  2116 			if ( widget.get( 'is_multi' ) ) {
  2121 			if ( widget.get( 'is_multi' ) ) {
  2117 				settingId += '[' + widgetNumber + ']';
  2122 				settingId += '[' + widgetNumber + ']';
  2118 			}
  2123 			}
  2119 			controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
  2124 			controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
  2120 
  2125 
  2121 			// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
  2126 			// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget).
  2122 			isExistingWidget = api.has( settingId );
  2127 			isExistingWidget = api.has( settingId );
  2123 			if ( ! isExistingWidget ) {
  2128 			if ( ! isExistingWidget ) {
  2124 				settingArgs = {
  2129 				settingArgs = {
  2125 					transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
  2130 					transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
  2126 					previewer: this.setting.previewer
  2131 					previewer: this.setting.previewer
  2127 				};
  2132 				};
  2128 				setting = api.create( settingId, settingId, '', settingArgs );
  2133 				setting = api.create( settingId, settingId, '', settingArgs );
  2129 				setting.set( {} ); // mark dirty, changing from '' to {}
  2134 				setting.set( {} ); // Mark dirty, changing from '' to {}.
  2130 			}
  2135 			}
  2131 
  2136 
  2132 			controlConstructor = api.controlConstructor[controlType];
  2137 			controlConstructor = api.controlConstructor[controlType];
  2133 			widgetFormControl = new controlConstructor( settingId, {
  2138 			widgetFormControl = new controlConstructor( settingId, {
  2134 				settings: {
  2139 				settings: {
  2144 				height: widget.get( 'height' ),
  2149 				height: widget.get( 'height' ),
  2145 				is_wide: widget.get( 'is_wide' )
  2150 				is_wide: widget.get( 'is_wide' )
  2146 			} );
  2151 			} );
  2147 			api.control.add( widgetFormControl );
  2152 			api.control.add( widgetFormControl );
  2148 
  2153 
  2149 			// Make sure widget is removed from the other sidebars
  2154 			// Make sure widget is removed from the other sidebars.
  2150 			api.each( function( otherSetting ) {
  2155 			api.each( function( otherSetting ) {
  2151 				if ( otherSetting.id === self.setting.id ) {
  2156 				if ( otherSetting.id === self.setting.id ) {
  2152 					return;
  2157 					return;
  2153 				}
  2158 				}
  2154 
  2159 
  2163 					otherSidebarWidgets.splice( i );
  2168 					otherSidebarWidgets.splice( i );
  2164 					otherSetting( otherSidebarWidgets );
  2169 					otherSetting( otherSidebarWidgets );
  2165 				}
  2170 				}
  2166 			} );
  2171 			} );
  2167 
  2172 
  2168 			// Add widget to this sidebar
  2173 			// Add widget to this sidebar.
  2169 			sidebarWidgets = this.setting().slice();
  2174 			sidebarWidgets = this.setting().slice();
  2170 			if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) {
  2175 			if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) {
  2171 				sidebarWidgets.push( widgetId );
  2176 				sidebarWidgets.push( widgetId );
  2172 				this.setting( sidebarWidgets );
  2177 				this.setting( sidebarWidgets );
  2173 			}
  2178 			}
  2182 
  2187 
  2183 			return widgetFormControl;
  2188 			return widgetFormControl;
  2184 		}
  2189 		}
  2185 	} );
  2190 	} );
  2186 
  2191 
  2187 	// Register models for custom panel, section, and control types
  2192 	// Register models for custom panel, section, and control types.
  2188 	$.extend( api.panelConstructor, {
  2193 	$.extend( api.panelConstructor, {
  2189 		widgets: api.Widgets.WidgetsPanel
  2194 		widgets: api.Widgets.WidgetsPanel
  2190 	});
  2195 	});
  2191 	$.extend( api.sectionConstructor, {
  2196 	$.extend( api.sectionConstructor, {
  2192 		sidebar: api.Widgets.SidebarSection
  2197 		sidebar: api.Widgets.SidebarSection
  2198 
  2203 
  2199 	/**
  2204 	/**
  2200 	 * Init Customizer for widgets.
  2205 	 * Init Customizer for widgets.
  2201 	 */
  2206 	 */
  2202 	api.bind( 'ready', function() {
  2207 	api.bind( 'ready', function() {
  2203 		// Set up the widgets panel
  2208 		// Set up the widgets panel.
  2204 		api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({
  2209 		api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({
  2205 			collection: api.Widgets.availableWidgets
  2210 			collection: api.Widgets.availableWidgets
  2206 		});
  2211 		});
  2207 
  2212 
  2208 		// Highlight widget control
  2213 		// Highlight widget control.
  2209 		api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
  2214 		api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
  2210 
  2215 
  2211 		// Open and focus widget control
  2216 		// Open and focus widget control.
  2212 		api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
  2217 		api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
  2213 	} );
  2218 	} );
  2214 
  2219 
  2215 	/**
  2220 	/**
  2216 	 * Highlight a widget control.
  2221 	 * Highlight a widget control.
  2239 	},
  2244 	},
  2240 
  2245 
  2241 	/**
  2246 	/**
  2242 	 * Given a widget control, find the sidebar widgets control that contains it.
  2247 	 * Given a widget control, find the sidebar widgets control that contains it.
  2243 	 * @param {string} widgetId
  2248 	 * @param {string} widgetId
  2244 	 * @return {object|null}
  2249 	 * @return {Object|null}
  2245 	 */
  2250 	 */
  2246 	api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) {
  2251 	api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) {
  2247 		var foundControl = null;
  2252 		var foundControl = null;
  2248 
  2253 
  2249 		// @todo this can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl()
  2254 		// @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl().
  2250 		api.control.each( function( control ) {
  2255 		api.control.each( function( control ) {
  2251 			if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) {
  2256 			if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) {
  2252 				foundControl = control;
  2257 				foundControl = control;
  2253 			}
  2258 			}
  2254 		} );
  2259 		} );
  2258 
  2263 
  2259 	/**
  2264 	/**
  2260 	 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it.
  2265 	 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it.
  2261 	 *
  2266 	 *
  2262 	 * @param {string} widgetId
  2267 	 * @param {string} widgetId
  2263 	 * @return {object|null}
  2268 	 * @return {Object|null}
  2264 	 */
  2269 	 */
  2265 	api.Widgets.getWidgetFormControlForWidget = function( widgetId ) {
  2270 	api.Widgets.getWidgetFormControlForWidget = function( widgetId ) {
  2266 		var foundControl = null;
  2271 		var foundControl = null;
  2267 
  2272 
  2268 		// @todo We can just use widgetIdToSettingId() here
  2273 		// @todo We can just use widgetIdToSettingId() here.
  2269 		api.control.each( function( control ) {
  2274 		api.control.each( function( control ) {
  2270 			if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) {
  2275 			if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) {
  2271 				foundControl = control;
  2276 				foundControl = control;
  2272 			}
  2277 			}
  2273 		} );
  2278 		} );
  2326 		}
  2331 		}
  2327 		focusConstruct.expanded.bind( onceCollapsed );
  2332 		focusConstruct.expanded.bind( onceCollapsed );
  2328 	}
  2333 	}
  2329 
  2334 
  2330 	/**
  2335 	/**
  2331 	 * @param {String} widgetId
  2336 	 * @param {string} widgetId
  2332 	 * @returns {Object}
  2337 	 * @return {Object}
  2333 	 */
  2338 	 */
  2334 	function parseWidgetId( widgetId ) {
  2339 	function parseWidgetId( widgetId ) {
  2335 		var matches, parsed = {
  2340 		var matches, parsed = {
  2336 			number: null,
  2341 			number: null,
  2337 			id_base: null
  2342 			id_base: null
  2340 		matches = widgetId.match( /^(.+)-(\d+)$/ );
  2345 		matches = widgetId.match( /^(.+)-(\d+)$/ );
  2341 		if ( matches ) {
  2346 		if ( matches ) {
  2342 			parsed.id_base = matches[1];
  2347 			parsed.id_base = matches[1];
  2343 			parsed.number = parseInt( matches[2], 10 );
  2348 			parsed.number = parseInt( matches[2], 10 );
  2344 		} else {
  2349 		} else {
  2345 			// likely an old single widget
  2350 			// Likely an old single widget.
  2346 			parsed.id_base = widgetId;
  2351 			parsed.id_base = widgetId;
  2347 		}
  2352 		}
  2348 
  2353 
  2349 		return parsed;
  2354 		return parsed;
  2350 	}
  2355 	}
  2351 
  2356 
  2352 	/**
  2357 	/**
  2353 	 * @param {String} widgetId
  2358 	 * @param {string} widgetId
  2354 	 * @returns {String} settingId
  2359 	 * @return {string} settingId
  2355 	 */
  2360 	 */
  2356 	function widgetIdToSettingId( widgetId ) {
  2361 	function widgetIdToSettingId( widgetId ) {
  2357 		var parsed = parseWidgetId( widgetId ), settingId;
  2362 		var parsed = parseWidgetId( widgetId ), settingId;
  2358 
  2363 
  2359 		settingId = 'widget_' + parsed.id_base;
  2364 		settingId = 'widget_' + parsed.id_base;