ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

customize-preview-widgets.js (23253B)


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