wp/wp-includes/js/customize-preview-widgets.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
     1 (function( wp, $ ){
     1 /* global _wpWidgetCustomizerPreviewSettings */
     2 
     2 
     3 	if ( ! wp || ! wp.customize ) { return; }
     3 /** @namespace wp.customize.widgetsPreview */
     4 
     4 wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
     5 	var api = wp.customize,
     5 
     6 		OldPreview;
     6 	var self;
     7 
     7 
     8 	/**
     8 	self = {
     9 	 * wp.customize.WidgetCustomizerPreview
     9 		renderedSidebars: {},
    10 	 *
    10 		renderedWidgets: {},
    11 	 */
    11 		registeredSidebars: [],
    12 	api.WidgetCustomizerPreview = {
    12 		registeredWidgets: {},
    13 		renderedSidebars: {}, // @todo Make rendered a property of the Backbone model
       
    14 		renderedWidgets: {}, // @todo Make rendered a property of the Backbone model
       
    15 		registeredSidebars: [], // @todo Make a Backbone collection
       
    16 		registeredWidgets: {}, // @todo Make array, Backbone collection
       
    17 		widgetSelectors: [],
    13 		widgetSelectors: [],
    18 		preview: null,
    14 		preview: null,
    19 		l10n: {},
    15 		l10n: {
    20 
    16 			widgetTooltip: ''
    21 		init: function () {
    17 		},
    22 			var self = this;
    18 		selectiveRefreshableWidgets: {}
    23 			this.buildWidgetSelectors();
    19 	};
    24 			this.highlightControls();
    20 
    25 
    21 	/**
    26 			this.preview.bind( 'highlight-widget', self.highlightWidget );
    22 	 * Init widgets preview.
    27 		},
    23 	 *
    28 
    24 	 * @since 4.5.0
    29 		/**
    25 	 */
    30 		 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info
    26 	self.init = function() {
    31 		 */
    27 		var self = this;
    32 		buildWidgetSelectors: function () {
    28 
    33 			var self = this;
    29 		self.preview = api.preview;
    34 
    30 		if ( ! _.isEmpty( self.selectiveRefreshableWidgets ) ) {
    35 			$.each( this.registeredSidebars, function ( i, sidebar ) {
    31 			self.addPartials();
    36 				var widgetTpl = [
    32 		}
    37 						sidebar.before_widget.replace('%1$s', '').replace('%2$s', ''),
    33 
    38 						sidebar.before_title,
    34 		self.buildWidgetSelectors();
    39 						sidebar.after_title,
    35 		self.highlightControls();
    40 						sidebar.after_widget
    36 
    41 					].join(''),
    37 		self.preview.bind( 'highlight-widget', self.highlightWidget );
    42 					emptyWidget,
    38 
    43 					widgetSelector,
    39 		api.preview.bind( 'active', function() {
    44 					widgetClasses;
    40 			self.highlightControls();
    45 
    41 		} );
    46 				emptyWidget = $(widgetTpl);
    42 
    47 				widgetSelector = emptyWidget.prop('tagName');
    43 		/*
    48 				widgetClasses = emptyWidget.prop('className');
    44 		 * Refresh a partial when the controls pane requests it. This is used currently just by the
    49 
    45 		 * Gallery widget so that when an attachment's caption is updated in the media modal,
    50 				// Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
    46 		 * the widget in the preview will then be refreshed to show the change. Normally doing this
    51 				if ( ! widgetClasses ) {
    47 		 * would not be necessary because all of the state should be contained inside the changeset,
       
    48 		 * as everything done in the Customizer should not make a change to the site unless the
       
    49 		 * changeset itself is published. Attachments are a current exception to this rule.
       
    50 		 * For a proposal to include attachments in the customized state, see #37887.
       
    51 		 */
       
    52 		api.preview.bind( 'refresh-widget-partial', function( widgetId ) {
       
    53 			var partialId = 'widget[' + widgetId + ']';
       
    54 			if ( api.selectiveRefresh.partial.has( partialId ) ) {
       
    55 				api.selectiveRefresh.partial( partialId ).refresh();
       
    56 			} else if ( self.renderedWidgets[ widgetId ] ) {
       
    57 				api.preview.send( 'refresh' ); // Fallback in case theme does not support 'customize-selective-refresh-widgets'.
       
    58 			}
       
    59 		} );
       
    60 	};
       
    61 
       
    62 	/**
       
    63 	 * Partial representing a widget instance.
       
    64 	 *
       
    65 	 * @memberOf wp.customize.widgetsPreview
       
    66 	 * @alias wp.customize.widgetsPreview.WidgetPartial
       
    67 	 *
       
    68 	 * @class
       
    69 	 * @augments wp.customize.selectiveRefresh.Partial
       
    70 	 * @since 4.5.0
       
    71 	 */
       
    72 	self.WidgetPartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.widgetsPreview.WidgetPartial.prototype */{
       
    73 
       
    74 		/**
       
    75 		 * Constructor.
       
    76 		 *
       
    77 		 * @since 4.5.0
       
    78 		 * @param {string} id - Partial ID.
       
    79 		 * @param {Object} options
       
    80 		 * @param {Object} options.params
       
    81 		 */
       
    82 		initialize: function( id, options ) {
       
    83 			var partial = this, matches;
       
    84 			matches = id.match( /^widget\[(.+)]$/ );
       
    85 			if ( ! matches ) {
       
    86 				throw new Error( 'Illegal id for widget partial.' );
       
    87 			}
       
    88 
       
    89 			partial.widgetId = matches[1];
       
    90 			partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
       
    91 			options = options || {};
       
    92 			options.params = _.extend(
       
    93 				{
       
    94 					settings: [ self.getWidgetSettingId( partial.widgetId ) ],
       
    95 					containerInclusive: true
       
    96 				},
       
    97 				options.params || {}
       
    98 			);
       
    99 
       
   100 			api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
       
   101 		},
       
   102 
       
   103 		/**
       
   104 		 * Refresh widget partial.
       
   105 		 *
       
   106 		 * @returns {Promise}
       
   107 		 */
       
   108 		refresh: function() {
       
   109 			var partial = this, refreshDeferred;
       
   110 			if ( ! self.selectiveRefreshableWidgets[ partial.widgetIdParts.idBase ] ) {
       
   111 				refreshDeferred = $.Deferred();
       
   112 				refreshDeferred.reject();
       
   113 				partial.fallback();
       
   114 				return refreshDeferred.promise();
       
   115 			} else {
       
   116 				return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
       
   117 			}
       
   118 		},
       
   119 
       
   120 		/**
       
   121 		 * Send widget-updated message to parent so spinner will get removed from widget control.
       
   122 		 *
       
   123 		 * @inheritdoc
       
   124 		 * @param {wp.customize.selectiveRefresh.Placement} placement
       
   125 		 */
       
   126 		renderContent: function( placement ) {
       
   127 			var partial = this;
       
   128 			if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
       
   129 				api.preview.send( 'widget-updated', partial.widgetId );
       
   130 				api.selectiveRefresh.trigger( 'widget-updated', partial );
       
   131 			}
       
   132 		}
       
   133 	});
       
   134 
       
   135 	/**
       
   136 	 * Partial representing a widget area.
       
   137 	 *
       
   138 	 * @memberOf wp.customize.widgetsPreview
       
   139 	 * @alias wp.customize.widgetsPreview.SidebarPartial
       
   140 	 *
       
   141 	 * @class
       
   142 	 * @augments wp.customize.selectiveRefresh.Partial
       
   143 	 * @since 4.5.0
       
   144 	 */
       
   145 	self.SidebarPartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.widgetsPreview.SidebarPartial.prototype */{
       
   146 
       
   147 		/**
       
   148 		 * Constructor.
       
   149 		 *
       
   150 		 * @since 4.5.0
       
   151 		 * @param {string} id - Partial ID.
       
   152 		 * @param {Object} options
       
   153 		 * @param {Object} options.params
       
   154 		 */
       
   155 		initialize: function( id, options ) {
       
   156 			var partial = this, matches;
       
   157 			matches = id.match( /^sidebar\[(.+)]$/ );
       
   158 			if ( ! matches ) {
       
   159 				throw new Error( 'Illegal id for sidebar partial.' );
       
   160 			}
       
   161 			partial.sidebarId = matches[1];
       
   162 
       
   163 			options = options || {};
       
   164 			options.params = _.extend(
       
   165 				{
       
   166 					settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
       
   167 				},
       
   168 				options.params || {}
       
   169 			);
       
   170 
       
   171 			api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
       
   172 
       
   173 			if ( ! partial.params.sidebarArgs ) {
       
   174 				throw new Error( 'The sidebarArgs param was not provided.' );
       
   175 			}
       
   176 			if ( partial.params.settings.length > 1 ) {
       
   177 				throw new Error( 'Expected SidebarPartial to only have one associated setting' );
       
   178 			}
       
   179 		},
       
   180 
       
   181 		/**
       
   182 		 * Set up the partial.
       
   183 		 *
       
   184 		 * @since 4.5.0
       
   185 		 */
       
   186 		ready: function() {
       
   187 			var sidebarPartial = this;
       
   188 
       
   189 			// Watch for changes to the sidebar_widgets setting.
       
   190 			_.each( sidebarPartial.settings(), function( settingId ) {
       
   191 				api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
       
   192 			} );
       
   193 
       
   194 			// Trigger an event for this sidebar being updated whenever a widget inside is rendered.
       
   195 			api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
       
   196 				var isAssignedWidgetPartial = (
       
   197 					placement.partial.extended( self.WidgetPartial ) &&
       
   198 					( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
       
   199 				);
       
   200 				if ( isAssignedWidgetPartial ) {
       
   201 					api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
       
   202 				}
       
   203 			} );
       
   204 
       
   205 			// Make sure that a widget partial has a container in the DOM prior to a refresh.
       
   206 			api.bind( 'change', function( widgetSetting ) {
       
   207 				var widgetId, parsedId;
       
   208 				parsedId = self.parseWidgetSettingId( widgetSetting.id );
       
   209 				if ( ! parsedId ) {
    52 					return;
   210 					return;
    53 				}
   211 				}
    54 
   212 				widgetId = parsedId.idBase;
    55 				widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, '');
   213 				if ( parsedId.number ) {
    56 
   214 					widgetId += '-' + String( parsedId.number );
    57 				if ( widgetClasses ) {
   215 				}
    58 					widgetSelector += '.' + widgetClasses.split(/\s+/).join('.');
   216 				if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
    59 				}
   217 					sidebarPartial.ensureWidgetPlacementContainers( widgetId );
    60 				self.widgetSelectors.push(widgetSelector);
   218 				}
    61 			});
   219 			} );
    62 		},
   220 		},
    63 
   221 
    64 		/**
   222 		/**
    65 		 * Highlight the widget on widget updates or widget control mouse overs.
   223 		 * Get the before/after boundary nodes for all instances of this sidebar (usually one).
    66 		 *
   224 		 *
    67 		 * @param  {string} widgetId ID of the widget.
   225 		 * Note that TreeWalker is not implemented in IE8.
    68 		 */
   226 		 *
    69 		highlightWidget: function( widgetId ) {
   227 		 * @since 4.5.0
    70 			var $body = $( document.body ),
   228 		 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
    71 				$widget = $( '#' + widgetId );
   229 		 */
    72 
   230 		findDynamicSidebarBoundaryNodes: function() {
    73 			$body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
   231 			var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
    74 
   232 			regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
    75 			$widget.addClass( 'widget-customizer-highlighted-widget' );
   233 			recursiveCommentTraversal = function( childNodes ) {
    76 			setTimeout( function () {
   234 				_.each( childNodes, function( node ) {
    77 				$widget.removeClass( 'widget-customizer-highlighted-widget' );
   235 					var matches;
    78 			}, 500 );
   236 					if ( 8 === node.nodeType ) {
    79 		},
   237 						matches = node.nodeValue.match( regExp );
    80 
   238 						if ( ! matches || matches[2] !== partial.sidebarId ) {
    81 		/**
   239 							return;
    82 		 * Show a title and highlight widgets on hover. On shift+clicking
   240 						}
    83 		 * focus the widget control.
   241 						if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
    84 		 */
   242 							boundaryNodes[ matches[3] ] = {
    85 		highlightControls: function() {
   243 								before: null,
    86 			var self = this,
   244 								after: null,
    87 				selector = this.widgetSelectors.join(',');
   245 								instanceNumber: parseInt( matches[3], 10 )
    88 
   246 							};
    89 			$(selector).attr( 'title', this.l10n.widgetTooltip );
   247 						}
    90 
   248 						if ( 'dynamic_sidebar_before' === matches[1] ) {
    91 			$(document).on( 'mouseenter', selector, function () {
   249 							boundaryNodes[ matches[3] ].before = node;
    92 				self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
   250 						} else {
    93 			});
   251 							boundaryNodes[ matches[3] ].after = node;
    94 
   252 						}
    95 			// Open expand the widget control when shift+clicking the widget element
   253 					} else if ( 1 === node.nodeType ) {
    96 			$(document).on( 'click', selector, function ( e ) {
   254 						recursiveCommentTraversal( node.childNodes );
    97 				if ( ! e.shiftKey ) {
   255 					}
       
   256 				} );
       
   257 			};
       
   258 
       
   259 			recursiveCommentTraversal( document.body.childNodes );
       
   260 			return _.values( boundaryNodes );
       
   261 		},
       
   262 
       
   263 		/**
       
   264 		 * Get the placements for this partial.
       
   265 		 *
       
   266 		 * @since 4.5.0
       
   267 		 * @returns {Array}
       
   268 		 */
       
   269 		placements: function() {
       
   270 			var partial = this;
       
   271 			return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
       
   272 				return new api.selectiveRefresh.Placement( {
       
   273 					partial: partial,
       
   274 					container: null,
       
   275 					startNode: boundaryNodes.before,
       
   276 					endNode: boundaryNodes.after,
       
   277 					context: {
       
   278 						instanceNumber: boundaryNodes.instanceNumber
       
   279 					}
       
   280 				} );
       
   281 			} );
       
   282 		},
       
   283 
       
   284 		/**
       
   285 		 * Get the list of widget IDs associated with this widget area.
       
   286 		 *
       
   287 		 * @since 4.5.0
       
   288 		 *
       
   289 		 * @returns {Array}
       
   290 		 */
       
   291 		getWidgetIds: function() {
       
   292 			var sidebarPartial = this, settingId, widgetIds;
       
   293 			settingId = sidebarPartial.settings()[0];
       
   294 			if ( ! settingId ) {
       
   295 				throw new Error( 'Missing associated setting.' );
       
   296 			}
       
   297 			if ( ! api.has( settingId ) ) {
       
   298 				throw new Error( 'Setting does not exist.' );
       
   299 			}
       
   300 			widgetIds = api( settingId ).get();
       
   301 			if ( ! _.isArray( widgetIds ) ) {
       
   302 				throw new Error( 'Expected setting to be array of widget IDs' );
       
   303 			}
       
   304 			return widgetIds.slice( 0 );
       
   305 		},
       
   306 
       
   307 		/**
       
   308 		 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
       
   309 		 *
       
   310 		 * @since 4.5.0
       
   311 		 *
       
   312 		 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
       
   313 		 */
       
   314 		reflowWidgets: function() {
       
   315 			var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
       
   316 			widgetIds = sidebarPartial.getWidgetIds();
       
   317 			sidebarPlacements = sidebarPartial.placements();
       
   318 
       
   319 			widgetPartials = {};
       
   320 			_.each( widgetIds, function( widgetId ) {
       
   321 				var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
       
   322 				if ( widgetPartial ) {
       
   323 					widgetPartials[ widgetId ] = widgetPartial;
       
   324 				}
       
   325 			} );
       
   326 
       
   327 			_.each( sidebarPlacements, function( sidebarPlacement ) {
       
   328 				var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
       
   329 
       
   330 				// Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
       
   331 				_.each( widgetPartials, function( widgetPartial ) {
       
   332 					_.each( widgetPartial.placements(), function( widgetPlacement ) {
       
   333 
       
   334 						if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
       
   335 							thisPosition = widgetPlacement.container.index();
       
   336 							sidebarWidgets.push( {
       
   337 								partial: widgetPartial,
       
   338 								placement: widgetPlacement,
       
   339 								position: thisPosition
       
   340 							} );
       
   341 							if ( thisPosition < lastPosition ) {
       
   342 								needsSort = true;
       
   343 							}
       
   344 							lastPosition = thisPosition;
       
   345 						}
       
   346 					} );
       
   347 				} );
       
   348 
       
   349 				if ( needsSort ) {
       
   350 					_.each( sidebarWidgets, function( sidebarWidget ) {
       
   351 						sidebarPlacement.endNode.parentNode.insertBefore(
       
   352 							sidebarWidget.placement.container[0],
       
   353 							sidebarPlacement.endNode
       
   354 						);
       
   355 
       
   356 						// @todo Rename partial-placement-moved?
       
   357 						api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
       
   358 					} );
       
   359 
       
   360 					sortedSidebarContainers.push( sidebarPlacement );
       
   361 				}
       
   362 			} );
       
   363 
       
   364 			if ( sortedSidebarContainers.length > 0 ) {
       
   365 				api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
       
   366 			}
       
   367 
       
   368 			return sortedSidebarContainers;
       
   369 		},
       
   370 
       
   371 		/**
       
   372 		 * Make sure there is a widget instance container in this sidebar for the given widget ID.
       
   373 		 *
       
   374 		 * @since 4.5.0
       
   375 		 *
       
   376 		 * @param {string} widgetId
       
   377 		 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
       
   378 		 */
       
   379 		ensureWidgetPlacementContainers: function( widgetId ) {
       
   380 			var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
       
   381 			widgetPartial = api.selectiveRefresh.partial( partialId );
       
   382 			if ( ! widgetPartial ) {
       
   383 				widgetPartial = new self.WidgetPartial( partialId, {
       
   384 					params: {}
       
   385 				} );
       
   386 			}
       
   387 
       
   388 			// Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
       
   389 			_.each( sidebarPartial.placements(), function( sidebarPlacement ) {
       
   390 				var foundWidgetPlacement, widgetContainerElement;
       
   391 
       
   392 				foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
       
   393 					return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
       
   394 				} );
       
   395 				if ( foundWidgetPlacement ) {
    98 					return;
   396 					return;
    99 				}
   397 				}
   100 				e.preventDefault();
   398 
   101 
   399 				widgetContainerElement = $(
   102 				self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
   400 					sidebarPartial.params.sidebarArgs.before_widget.replace( /%1\$s/g, widgetId ).replace( /%2\$s/g, 'widget' ) +
   103 			});
   401 					sidebarPartial.params.sidebarArgs.after_widget
       
   402 				);
       
   403 
       
   404 				// Handle rare case where before_widget and after_widget are empty.
       
   405 				if ( ! widgetContainerElement[0] ) {
       
   406 					return;
       
   407 				}
       
   408 
       
   409 				widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
       
   410 				widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
       
   411 				widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
       
   412 
       
   413 				/*
       
   414 				 * Make sure the widget container element has the customize-container context data.
       
   415 				 * The sidebar_instance_number is used to disambiguate multiple instances of the
       
   416 				 * same sidebar are rendered onto the template, and so the same widget is embedded
       
   417 				 * multiple times.
       
   418 				 */
       
   419 				widgetContainerElement.data( 'customize-partial-placement-context', {
       
   420 					'sidebar_id': sidebarPartial.sidebarId,
       
   421 					'sidebar_instance_number': sidebarPlacement.context.instanceNumber
       
   422 				} );
       
   423 
       
   424 				sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
       
   425 				wasInserted = true;
       
   426 			} );
       
   427 
       
   428 			api.selectiveRefresh.partial.add( widgetPartial );
       
   429 
       
   430 			if ( wasInserted ) {
       
   431 				sidebarPartial.reflowWidgets();
       
   432 			}
       
   433 
       
   434 			return widgetPartial;
       
   435 		},
       
   436 
       
   437 		/**
       
   438 		 * Handle change to the sidebars_widgets[] setting.
       
   439 		 *
       
   440 		 * @since 4.5.0
       
   441 		 *
       
   442 		 * @param {Array} newWidgetIds New widget ids.
       
   443 		 * @param {Array} oldWidgetIds Old widget ids.
       
   444 		 */
       
   445 		handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
       
   446 			var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
       
   447 
       
   448 			needsRefresh = (
       
   449 				( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
       
   450 				( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
       
   451 			);
       
   452 			if ( needsRefresh ) {
       
   453 				sidebarPartial.fallback();
       
   454 				return;
       
   455 			}
       
   456 
       
   457 			// Handle removal of widgets.
       
   458 			widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
       
   459 			_.each( widgetsRemoved, function( removedWidgetId ) {
       
   460 				var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
       
   461 				if ( widgetPartial ) {
       
   462 					_.each( widgetPartial.placements(), function( placement ) {
       
   463 						var isRemoved = (
       
   464 							placement.context.sidebar_id === sidebarPartial.sidebarId ||
       
   465 							( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
       
   466 						);
       
   467 						if ( isRemoved ) {
       
   468 							placement.container.remove();
       
   469 						}
       
   470 					} );
       
   471 				}
       
   472 				delete self.renderedWidgets[ removedWidgetId ];
       
   473 			} );
       
   474 
       
   475 			// Handle insertion of widgets.
       
   476 			widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
       
   477 			_.each( widgetsAdded, function( addedWidgetId ) {
       
   478 				var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
       
   479 				addedWidgetPartials.push( widgetPartial );
       
   480 				self.renderedWidgets[ addedWidgetId ] = true;
       
   481 			} );
       
   482 
       
   483 			_.each( addedWidgetPartials, function( widgetPartial ) {
       
   484 				widgetPartial.refresh();
       
   485 			} );
       
   486 
       
   487 			api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
       
   488 		},
       
   489 
       
   490 		/**
       
   491 		 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
       
   492 		 *
       
   493 		 * @since 4.5.0
       
   494 		 */
       
   495 		refresh: function() {
       
   496 			var partial = this, deferred = $.Deferred();
       
   497 
       
   498 			deferred.fail( function() {
       
   499 				partial.fallback();
       
   500 			} );
       
   501 
       
   502 			if ( 0 === partial.placements().length ) {
       
   503 				deferred.reject();
       
   504 			} else {
       
   505 				_.each( partial.reflowWidgets(), function( sidebarPlacement ) {
       
   506 					api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
       
   507 				} );
       
   508 				deferred.resolve();
       
   509 			}
       
   510 
       
   511 			return deferred.promise();
   104 		}
   512 		}
   105 	};
   513 	});
   106 
   514 
   107 	/**
   515 	api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
   108 	 * Capture the instance of the Preview since it is private
   516 	api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
   109 	 */
   517 
   110 	OldPreview = api.Preview;
   518 	/**
   111 	api.Preview = OldPreview.extend( {
   519 	 * Add partials for the registered widget areas (sidebars).
   112 		initialize: function( params, options ) {
   520 	 *
   113 			api.WidgetCustomizerPreview.preview = this;
   521 	 * @since 4.5.0
   114 			OldPreview.prototype.initialize.call( this, params, options );
   522 	 */
   115 		}
   523 	self.addPartials = function() {
   116 	} );
   524 		_.each( self.registeredSidebars, function( registeredSidebar ) {
   117 
   525 			var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
   118 	$(function () {
   526 			partial = api.selectiveRefresh.partial( partialId );
   119 		var settings = window._wpWidgetCustomizerPreviewSettings;
   527 			if ( ! partial ) {
   120 		if ( ! settings ) {
   528 				partial = new self.SidebarPartial( partialId, {
       
   529 					params: {
       
   530 						sidebarArgs: registeredSidebar
       
   531 					}
       
   532 				} );
       
   533 				api.selectiveRefresh.partial.add( partial );
       
   534 			}
       
   535 		} );
       
   536 	};
       
   537 
       
   538 	/**
       
   539 	 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
       
   540 	 *
       
   541 	 * @memberOf wp.customize.widgetsPreview
       
   542 	 *
       
   543 	 * @since 3.9.0
       
   544 	 */
       
   545 	self.buildWidgetSelectors = function() {
       
   546 		var self = this;
       
   547 
       
   548 		$.each( self.registeredSidebars, function( i, sidebar ) {
       
   549 			var widgetTpl = [
       
   550 					sidebar.before_widget,
       
   551 					sidebar.before_title,
       
   552 					sidebar.after_title,
       
   553 					sidebar.after_widget
       
   554 				].join( '' ),
       
   555 				emptyWidget,
       
   556 				widgetSelector,
       
   557 				widgetClasses;
       
   558 
       
   559 			emptyWidget = $( widgetTpl );
       
   560 			widgetSelector = emptyWidget.prop( 'tagName' ) || '';
       
   561 			widgetClasses = emptyWidget.prop( 'className' ) || '';
       
   562 
       
   563 			// Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
       
   564 			if ( ! widgetClasses ) {
       
   565 				return;
       
   566 			}
       
   567 
       
   568 			// Remove class names that incorporate the string formatting placeholders %1$s and %2$s.
       
   569 			widgetClasses = widgetClasses.replace( /\S*%[12]\$s\S*/g, '' );
       
   570 			widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
       
   571 			if ( widgetClasses ) {
       
   572 				widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
       
   573 			}
       
   574 			self.widgetSelectors.push( widgetSelector );
       
   575 		});
       
   576 	};
       
   577 
       
   578 	/**
       
   579 	 * Highlight the widget on widget updates or widget control mouse overs.
       
   580 	 *
       
   581 	 * @memberOf wp.customize.widgetsPreview
       
   582 	 *
       
   583 	 * @since 3.9.0
       
   584 	 * @param  {string} widgetId ID of the widget.
       
   585 	 */
       
   586 	self.highlightWidget = function( widgetId ) {
       
   587 		var $body = $( document.body ),
       
   588 			$widget = $( '#' + widgetId );
       
   589 
       
   590 		$body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
       
   591 
       
   592 		$widget.addClass( 'widget-customizer-highlighted-widget' );
       
   593 		setTimeout( function() {
       
   594 			$widget.removeClass( 'widget-customizer-highlighted-widget' );
       
   595 		}, 500 );
       
   596 	};
       
   597 
       
   598 	/**
       
   599 	 * Show a title and highlight widgets on hover. On shift+clicking
       
   600 	 * focus the widget control.
       
   601 	 *
       
   602 	 * @memberOf wp.customize.widgetsPreview
       
   603 	 *
       
   604 	 * @since 3.9.0
       
   605 	 */
       
   606 	self.highlightControls = function() {
       
   607 		var self = this,
       
   608 			selector = this.widgetSelectors.join( ',' );
       
   609 
       
   610 		// Skip adding highlights if not in the customizer preview iframe.
       
   611 		if ( ! api.settings.channel ) {
   121 			return;
   612 			return;
   122 		}
   613 		}
   123 
   614 
   124 		$.extend( api.WidgetCustomizerPreview, settings );
   615 		$( selector ).attr( 'title', this.l10n.widgetTooltip );
   125 
   616 
   126 		api.WidgetCustomizerPreview.init();
   617 		$( document ).on( 'mouseenter', selector, function() {
       
   618 			self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
       
   619 		});
       
   620 
       
   621 		// Open expand the widget control when shift+clicking the widget element
       
   622 		$( document ).on( 'click', selector, function( e ) {
       
   623 			if ( ! e.shiftKey ) {
       
   624 				return;
       
   625 			}
       
   626 			e.preventDefault();
       
   627 
       
   628 			self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
       
   629 		});
       
   630 	};
       
   631 
       
   632 	/**
       
   633 	 * Parse a widget ID.
       
   634 	 *
       
   635 	 * @memberOf wp.customize.widgetsPreview
       
   636 	 *
       
   637 	 * @since 4.5.0
       
   638 	 *
       
   639 	 * @param {string} widgetId Widget ID.
       
   640 	 * @returns {{idBase: string, number: number|null}}
       
   641 	 */
       
   642 	self.parseWidgetId = function( widgetId ) {
       
   643 		var matches, parsed = {
       
   644 			idBase: '',
       
   645 			number: null
       
   646 		};
       
   647 
       
   648 		matches = widgetId.match( /^(.+)-(\d+)$/ );
       
   649 		if ( matches ) {
       
   650 			parsed.idBase = matches[1];
       
   651 			parsed.number = parseInt( matches[2], 10 );
       
   652 		} else {
       
   653 			parsed.idBase = widgetId; // Likely an old single widget.
       
   654 		}
       
   655 
       
   656 		return parsed;
       
   657 	};
       
   658 
       
   659 	/**
       
   660 	 * Parse a widget setting ID.
       
   661 	 *
       
   662 	 * @memberOf wp.customize.widgetsPreview
       
   663 	 *
       
   664 	 * @since 4.5.0
       
   665 	 *
       
   666 	 * @param {string} settingId Widget setting ID.
       
   667 	 * @returns {{idBase: string, number: number|null}|null}
       
   668 	 */
       
   669 	self.parseWidgetSettingId = function( settingId ) {
       
   670 		var matches, parsed = {
       
   671 			idBase: '',
       
   672 			number: null
       
   673 		};
       
   674 
       
   675 		matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
       
   676 		if ( ! matches ) {
       
   677 			return null;
       
   678 		}
       
   679 		parsed.idBase = matches[1];
       
   680 		if ( matches[2] ) {
       
   681 			parsed.number = parseInt( matches[2], 10 );
       
   682 		}
       
   683 		return parsed;
       
   684 	};
       
   685 
       
   686 	/**
       
   687 	 * Convert a widget ID into a Customizer setting ID.
       
   688 	 *
       
   689 	 * @memberOf wp.customize.widgetsPreview
       
   690 	 *
       
   691 	 * @since 4.5.0
       
   692 	 *
       
   693 	 * @param {string} widgetId Widget ID.
       
   694 	 * @returns {string} settingId Setting ID.
       
   695 	 */
       
   696 	self.getWidgetSettingId = function( widgetId ) {
       
   697 		var parsed = this.parseWidgetId( widgetId ), settingId;
       
   698 
       
   699 		settingId = 'widget_' + parsed.idBase;
       
   700 		if ( parsed.number ) {
       
   701 			settingId += '[' + String( parsed.number ) + ']';
       
   702 		}
       
   703 
       
   704 		return settingId;
       
   705 	};
       
   706 
       
   707 	api.bind( 'preview-ready', function() {
       
   708 		$.extend( self, _wpWidgetCustomizerPreviewSettings );
       
   709 		self.init();
   127 	});
   710 	});
   128 
   711 
   129 })( window.wp, jQuery );
   712 	return self;
       
   713 })( jQuery, _, wp, wp.customize );