+ widgetContainerElement = $(
+ sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) +
+ sidebarPartial.params.sidebarArgs.after_widget
+ );
+
+ 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
+ * multiple times.
+ */
+ widgetContainerElement.data( 'customize-partial-placement-context', {
+ 'sidebar_id': sidebarPartial.sidebarId,
+ 'sidebar_instance_number': sidebarPlacement.context.instanceNumber
+ } );
+
+ sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
+ wasInserted = true;
+ } );
+
+ if ( wasInserted ) {
+ sidebarPartial.reflowWidgets();
+ }
+
+ return widgetPartial;
+ },
+
+ /**
+ * Handle change to the sidebars_widgets[] setting.
+ *
+ * @since 4.5.0
+ *
+ * @param {Array} newWidgetIds New widget ids.
+ * @param {Array} oldWidgetIds Old widget ids.
+ */
+ handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
+ var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
+
+ needsRefresh = (
+ ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
+ ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
+ );
+ if ( needsRefresh ) {
+ sidebarPartial.fallback();
+ return;
+ }
+
+ // Handle removal of widgets.
+ widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
+ _.each( widgetsRemoved, function( removedWidgetId ) {
+ var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
+ if ( widgetPartial ) {
+ _.each( widgetPartial.placements(), function( placement ) {
+ var isRemoved = (
+ placement.context.sidebar_id === sidebarPartial.sidebarId ||
+ ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
+ );
+ if ( isRemoved ) {
+ placement.container.remove();
+ }
+ } );
+ }
+ } );
+
+ // Handle insertion of widgets.
+ widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
+ _.each( widgetsAdded, function( addedWidgetId ) {
+ var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
+ addedWidgetPartials.push( widgetPartial );
+ } );
+
+ _.each( addedWidgetPartials, function( widgetPartial ) {
+ widgetPartial.refresh();
+ } );
+
+ api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
+ },
+
+ /**
+ * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
+ *
+ * @since 4.5.0
+ */
+ refresh: function() {
+ var partial = this, deferred = $.Deferred();
+
+ deferred.fail( function() {
+ partial.fallback();
+ } );
+
+ if ( 0 === partial.placements().length ) {
+ deferred.reject();
+ } else {
+ _.each( partial.reflowWidgets(), function( sidebarPlacement ) {
+ api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
+ } );
+ deferred.resolve();
+ }
+
+ return deferred.promise();
+ }
+ });
+
+ api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
+ api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
+
+ /**
+ * Add partials for the registered widget areas (sidebars).
+ *
+ * @since 4.5.0
+ */
+ self.addPartials = function() {
+ _.each( self.registeredSidebars, function( registeredSidebar ) {
+ var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
+ partial = api.selectiveRefresh.partial( partialId );
+ if ( ! partial ) {
+ partial = new self.SidebarPartial( partialId, {
+ params: {
+ sidebarArgs: registeredSidebar
+ }
+ } );
+ api.selectiveRefresh.partial.add( partial.id, partial );
+ }
+ } );
+ };
+
+ /**
+ * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
+ *
+ * @since 3.9.0
+ */
+ self.buildWidgetSelectors = function() {
+ var self = this;
+
+ $.each( self.registeredSidebars, function( i, sidebar ) {
+ var widgetTpl = [
+ sidebar.before_widget.replace( '%1$s', '' ).replace( '%2$s', '' ),
+ sidebar.before_title,
+ sidebar.after_title,
+ sidebar.after_widget
+ ].join( '' ),
+ emptyWidget,
+ widgetSelector,
+ widgetClasses;
+
+ emptyWidget = $( widgetTpl );
+ widgetSelector = emptyWidget.prop( 'tagName' );
+ widgetClasses = emptyWidget.prop( 'className' );
+
+ // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
+ if ( ! widgetClasses ) {
+ return;
+ }
+
+ widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
+
+ if ( widgetClasses ) {
+ widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
+ }
+ self.widgetSelectors.push( widgetSelector );
+ });
+ };
+
+ /**
+ * Highlight the widget on widget updates or widget control mouse overs.
+ *
+ * @since 3.9.0
+ * @param {string} widgetId ID of the widget.
+ */
+ self.highlightWidget = function( widgetId ) {
+ var $body = $( document.body ),
+ $widget = $( '#' + widgetId );
+
+ $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
+
+ $widget.addClass( 'widget-customizer-highlighted-widget' );
+ setTimeout( function() {
+ $widget.removeClass( 'widget-customizer-highlighted-widget' );
+ }, 500 );
+ };
+
+ /**
+ * Show a title and highlight widgets on hover. On shift+clicking
+ * focus the widget control.
+ *
+ * @since 3.9.0
+ */
+ self.highlightControls = function() {
+ var self = this,
+ selector = this.widgetSelectors.join( ',' );
+
+ $( selector ).attr( 'title', this.l10n.widgetTooltip );
+
+ $( document ).on( 'mouseenter', selector, function() {
+ self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
+ });
+
+ // Open expand the widget control when shift+clicking the widget element
+ $( document ).on( 'click', selector, function( e ) {
+ if ( ! e.shiftKey ) {
+ return;
+ }
+ e.preventDefault();
+
+ self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
+ });
+ };
+
+ /**
+ * Parse a widget ID.
+ *
+ * @since 4.5.0
+ *
+ * @param {string} widgetId Widget ID.
+ * @returns {{idBase: string, number: number|null}}
+ */
+ self.parseWidgetId = function( widgetId ) {
+ var matches, parsed = {
+ idBase: '',
+ number: null
+ };
+
+ matches = widgetId.match( /^(.+)-(\d+)$/ );
+ if ( matches ) {
+ parsed.idBase = matches[1];
+ parsed.number = parseInt( matches[2], 10 );
+ } else {
+ parsed.idBase = widgetId; // Likely an old single widget.