* @output wp-includes/js/customize-preview-widgets.js
/* global _wpWidgetCustomizerPreviewSettings */
* Handles the initialization, refreshing and rendering of widget partials and sidebar widgets.
* @namespace wp.customize.widgetsPreview
* @param {jQuery} $ The jQuery object.
* @param {Object} _ The utilities library.
* @param {Object} wp Current WordPress environment instance.
* @param {Object} api Information from the API.
* @return {Object} Widget-related variables.
wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
selectiveRefreshableWidgets: {}
* Initializes the widgets preview.
* @memberOf wp.customize.widgetsPreview
self.preview = api.preview;
if ( ! _.isEmpty( self.selectiveRefreshableWidgets ) ) {
self.buildWidgetSelectors();
self.highlightControls();
self.preview.bind( 'highlight-widget', self.highlightWidget );
api.preview.bind( 'active', function() {
self.highlightControls();
* Refresh a partial when the controls pane requests it. This is used currently just by the
* Gallery widget so that when an attachment's caption is updated in the media modal,
* the widget in the preview will then be refreshed to show the change. Normally doing this
* would not be necessary because all of the state should be contained inside the changeset,
* as everything done in the Customizer should not make a change to the site unless the
* changeset itself is published. Attachments are a current exception to this rule.
* For a proposal to include attachments in the customized state, see #37887.
api.preview.bind( 'refresh-widget-partial', function( widgetId ) {
var partialId = 'widget[' + widgetId + ']';
if ( api.selectiveRefresh.partial.has( partialId ) ) {
api.selectiveRefresh.partial( partialId ).refresh();
} else if ( self.renderedWidgets[ widgetId ] ) {
api.preview.send( 'refresh' ); // Fallback in case theme does not support 'customize-selective-refresh-widgets'.
self.WidgetPartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.widgetsPreview.WidgetPartial.prototype */{
* Represents a partial widget instance.
* @augments wp.customize.selectiveRefresh.Partial
* @alias wp.customize.widgetsPreview.WidgetPartial
* @memberOf wp.customize.widgetsPreview
* @param {string} id The partial's ID.
* @param {Object} options Options used to initialize the partial's
* @param {Object} options.params The options parameters.
initialize: function( id, options ) {
var partial = this, matches;
matches = id.match( /^widget\[(.+)]$/ );
throw new Error( 'Illegal id for widget partial.' );
partial.widgetId = matches[1];
partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
options.params = _.extend(
settings: [ self.getWidgetSettingId( partial.widgetId ) ],
api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
* Refreshes the widget partial.
* @return {Promise|void} Either a promise postponing the refresh, or void.
var partial = this, refreshDeferred;
if ( ! self.selectiveRefreshableWidgets[ partial.widgetIdParts.idBase ] ) {
refreshDeferred = $.Deferred();
refreshDeferred.reject();
return refreshDeferred.promise();
return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
* Sends the widget-updated message to the parent so the spinner will get
* removed from the widget control.
* @param {wp.customize.selectiveRefresh.Placement} placement The placement
renderContent: function( placement ) {
if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
api.preview.send( 'widget-updated', partial.widgetId );
api.selectiveRefresh.trigger( 'widget-updated', partial );
self.SidebarPartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.widgetsPreview.SidebarPartial.prototype */{
* Represents a partial widget area.
* @augments wp.customize.selectiveRefresh.Partial
* @memberOf wp.customize.widgetsPreview
* @alias wp.customize.widgetsPreview.SidebarPartial
* @param {string} id The partial's ID.
* @param {Object} options Options used to initialize the partial's instance.
* @param {Object} options.params The options parameters.
initialize: function( id, options ) {
var partial = this, matches;
matches = id.match( /^sidebar\[(.+)]$/ );
throw new Error( 'Illegal id for sidebar partial.' );
partial.sidebarId = matches[1];
options.params = _.extend(
settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
if ( ! partial.params.sidebarArgs ) {
throw new Error( 'The sidebarArgs param was not provided.' );
if ( partial.params.settings.length > 1 ) {
throw new Error( 'Expected SidebarPartial to only have one associated setting' );
var sidebarPartial = this;
// Watch for changes to the sidebar_widgets setting.
_.each( sidebarPartial.settings(), function( settingId ) {
api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
// Trigger an event for this sidebar being updated whenever a widget inside is rendered.
api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
var isAssignedWidgetPartial = (
placement.partial.extended( self.WidgetPartial ) &&
( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
if ( isAssignedWidgetPartial ) {
api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
// Make sure that a widget partial has a container in the DOM prior to a refresh.
api.bind( 'change', function( widgetSetting ) {
parsedId = self.parseWidgetSettingId( widgetSetting.id );
widgetId = parsedId.idBase;
widgetId += '-' + String( parsedId.number );
if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
sidebarPartial.ensureWidgetPlacementContainers( widgetId );
* Gets the before/after boundary nodes for all instances of this sidebar
* Note that TreeWalker is not implemented in IE8.
* @return {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
* An array with an object for each sidebar instance, containing the
* node before and after the sidebar instance and its instance number.
findDynamicSidebarBoundaryNodes: function() {
var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
recursiveCommentTraversal = function( childNodes ) {
_.each( childNodes, function( node ) {
if ( 8 === node.nodeType ) {
matches = node.nodeValue.match( regExp );
if ( ! matches || matches[2] !== partial.sidebarId ) {
if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
boundaryNodes[ matches[3] ] = {
instanceNumber: parseInt( matches[3], 10 )
if ( 'dynamic_sidebar_before' === matches[1] ) {
boundaryNodes[ matches[3] ].before = node;
boundaryNodes[ matches[3] ].after = node;
} else if ( 1 === node.nodeType ) {
recursiveCommentTraversal( node.childNodes );
recursiveCommentTraversal( document.body.childNodes );
return _.values( boundaryNodes );
* Gets the placements for this partial.
* @return {Array} An array containing placement objects for each of the
* dynamic sidebar boundary nodes.
return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
return new api.selectiveRefresh.Placement( {
startNode: boundaryNodes.before,
endNode: boundaryNodes.after,
instanceNumber: boundaryNodes.instanceNumber
* Get the list of widget IDs associated with this widget area.
* @throws {Error} If there's no settingId.
* @throws {Error} If the setting doesn't exist in the API.
* @throws {Error} If the API doesn't pass an array of widget IDs.
* @return {Array} A shallow copy of the array containing widget IDs.
getWidgetIds: function() {
var sidebarPartial = this, settingId, widgetIds;
settingId = sidebarPartial.settings()[0];
throw new Error( 'Missing associated setting.' );
if ( ! api.has( settingId ) ) {
throw new Error( 'Setting does not exist.' );
widgetIds = api( settingId ).get();
if ( ! _.isArray( widgetIds ) ) {
throw new Error( 'Expected setting to be array of widget IDs' );
return widgetIds.slice( 0 );
* Reflows widgets in the sidebar, ensuring they have the proper position in the
* @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements
reflowWidgets: function() {
var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
widgetIds = sidebarPartial.getWidgetIds();
sidebarPlacements = sidebarPartial.placements();
_.each( widgetIds, function( widgetId ) {
var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
widgetPartials[ widgetId ] = widgetPartial;
_.each( sidebarPlacements, function( sidebarPlacement ) {
var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
// Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
_.each( widgetPartials, function( widgetPartial ) {
_.each( widgetPartial.placements(), function( widgetPlacement ) {
if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
thisPosition = widgetPlacement.container.index();
placement: widgetPlacement,
if ( thisPosition < lastPosition ) {
lastPosition = thisPosition;
_.each( sidebarWidgets, function( sidebarWidget ) {
sidebarPlacement.endNode.parentNode.insertBefore(
sidebarWidget.placement.container[0],
// @todo Rename partial-placement-moved?
api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
sortedSidebarContainers.push( sidebarPlacement );
if ( sortedSidebarContainers.length > 0 ) {
api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
return sortedSidebarContainers;
* Makes sure there is a widget instance container in this sidebar for the given
* @param {string} widgetId The widget ID.
* @return {wp.customize.selectiveRefresh.Partial} The widget instance partial.
ensureWidgetPlacementContainers: function( widgetId ) {
var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
widgetPartial = api.selectiveRefresh.partial( partialId );
widgetPartial = new self.WidgetPartial( partialId, {
// Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
_.each( sidebarPartial.placements(), function( sidebarPlacement ) {
var foundWidgetPlacement, widgetContainerElement;
foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
if ( foundWidgetPlacement ) {
widgetContainerElement = $(
sidebarPartial.params.sidebarArgs.before_widget.replace( /%1\$s/g, widgetId ).replace( /%2\$s/g, 'widget' ) +
sidebarPartial.params.sidebarArgs.after_widget
// Handle rare case where before_widget and after_widget are empty.
if ( ! widgetContainerElement[0] ) {
widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
* Make sure the widget container element has the customize-container context data.
* The sidebar_instance_number is used to disambiguate multiple instances of the
* same sidebar are rendered onto the template, and so the same widget is embedded
widgetContainerElement.data( 'customize-partial-placement-context', {
'sidebar_id': sidebarPartial.sidebarId,
'sidebar_instance_number': sidebarPlacement.context.instanceNumber
sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
api.selectiveRefresh.partial.add( widgetPartial );
sidebarPartial.reflowWidgets();
* Handles changes to the sidebars_widgets[] setting.
* @param {Array} newWidgetIds New widget IDs.
* @param {Array} oldWidgetIds Old widget IDs.
handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
sidebarPartial.fallback();
// Handle removal of widgets.
widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
_.each( widgetsRemoved, function( removedWidgetId ) {
var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );