summaryrefslogtreecommitdiffstats
path: root/wp-includes/js/customize-preview-widgets.js
diff options
context:
space:
mode:
Diffstat (limited to 'wp-includes/js/customize-preview-widgets.js')
-rw-r--r--wp-includes/js/customize-preview-widgets.js773
1 files changed, 773 insertions, 0 deletions
diff --git a/wp-includes/js/customize-preview-widgets.js b/wp-includes/js/customize-preview-widgets.js
new file mode 100644
index 0000000..e1191a6
--- /dev/null
+++ b/wp-includes/js/customize-preview-widgets.js
@@ -0,0 +1,773 @@
+/**
+ * @output wp-includes/js/customize-preview-widgets.js
+ */
+
+/* global _wpWidgetCustomizerPreviewSettings */
+
+/**
+ * Handles the initialization, refreshing and rendering of widget partials and sidebar widgets.
+ *
+ * @since 4.5.0
+ *
+ * @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 ) {
+
+ var self;
+
+ self = {
+ renderedSidebars: {},
+ renderedWidgets: {},
+ registeredSidebars: [],
+ registeredWidgets: {},
+ widgetSelectors: [],
+ preview: null,
+ l10n: {
+ widgetTooltip: ''
+ },
+ selectiveRefreshableWidgets: {}
+ };
+
+ /**
+ * Initializes the widgets preview.
+ *
+ * @since 4.5.0
+ *
+ * @memberOf wp.customize.widgetsPreview
+ *
+ * @return {void}
+ */
+ self.init = function() {
+ var self = this;
+
+ self.preview = api.preview;
+ if ( ! _.isEmpty( self.selectiveRefreshableWidgets ) ) {
+ self.addPartials();
+ }
+
+ 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.
+ *
+ * @since 4.5.0
+ *
+ * @constructs
+ * @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
+ * instance.
+ * @param {Object} options.params The options parameters.
+ */
+ initialize: function( id, options ) {
+ var partial = this, matches;
+ matches = id.match( /^widget\[(.+)]$/ );
+ if ( ! matches ) {
+ throw new Error( 'Illegal id for widget partial.' );
+ }
+
+ partial.widgetId = matches[1];
+ partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
+ options = options || {};
+ options.params = _.extend(
+ {
+ settings: [ self.getWidgetSettingId( partial.widgetId ) ],
+ containerInclusive: true
+ },
+ options.params || {}
+ );
+
+ api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
+ },
+
+ /**
+ * Refreshes the widget partial.
+ *
+ * @since 4.5.0
+ *
+ * @return {Promise|void} Either a promise postponing the refresh, or void.
+ */
+ refresh: function() {
+ var partial = this, refreshDeferred;
+ if ( ! self.selectiveRefreshableWidgets[ partial.widgetIdParts.idBase ] ) {
+ refreshDeferred = $.Deferred();
+ refreshDeferred.reject();
+ partial.fallback();
+ return refreshDeferred.promise();
+ } else {
+ 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.
+ *
+ * @inheritDoc
+ * @param {wp.customize.selectiveRefresh.Placement} placement The placement
+ * function.
+ *
+ * @return {void}
+ */
+ renderContent: function( placement ) {
+ var partial = this;
+ 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.
+ *
+ * @since 4.5.0
+ *
+ * @class
+ * @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\[(.+)]$/ );
+ if ( ! matches ) {
+ throw new Error( 'Illegal id for sidebar partial.' );
+ }
+ partial.sidebarId = matches[1];
+
+ options = options || {};
+ options.params = _.extend(
+ {
+ settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
+ },
+ options.params || {}
+ );
+
+ 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' );
+ }
+ },
+
+ /**
+ * Sets up the partial.
+ *
+ * @since 4.5.0
+ *
+ * @return {void}
+ */
+ ready: function() {
+ 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 ) {
+ var widgetId, parsedId;
+ parsedId = self.parseWidgetSettingId( widgetSetting.id );
+ if ( ! parsedId ) {
+ return;
+ }
+ widgetId = parsedId.idBase;
+ if ( parsedId.number ) {
+ 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
+ * (usually one).
+ *
+ * Note that TreeWalker is not implemented in IE8.
+ *
+ * @since 4.5.0
+ *
+ * @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 ) {
+ var matches;
+ if ( 8 === node.nodeType ) {
+ matches = node.nodeValue.match( regExp );
+ if ( ! matches || matches[2] !== partial.sidebarId ) {
+ return;
+ }
+ if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
+ boundaryNodes[ matches[3] ] = {
+ before: null,
+ after: null,
+ instanceNumber: parseInt( matches[3], 10 )
+ };
+ }
+ if ( 'dynamic_sidebar_before' === matches[1] ) {
+ boundaryNodes[ matches[3] ].before = node;
+ } else {
+ 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.
+ *
+ * @since 4.5.0
+ *
+ * @return {Array} An array containing placement objects for each of the
+ * dynamic sidebar boundary nodes.
+ */
+ placements: function() {
+ var partial = this;
+ return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
+ return new api.selectiveRefresh.Placement( {
+ partial: partial,
+ container: null,
+ startNode: boundaryNodes.before,
+ endNode: boundaryNodes.after,
+ context: {
+ instanceNumber: boundaryNodes.instanceNumber
+ }
+ } );
+ } );
+ },
+
+ /**
+ * Get the list of widget IDs associated with this widget area.
+ *
+ * @since 4.5.0
+ *
+ * @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];
+ if ( ! settingId ) {
+ 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
+ * DOM.
+ *
+ * @since 4.5.0
+ *
+ * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements
+ * that were reflowed.
+ */
+ reflowWidgets: function() {
+ var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
+ widgetIds = sidebarPartial.getWidgetIds();
+ sidebarPlacements = sidebarPartial.placements();
+
+ widgetPartials = {};
+ _.each( widgetIds, function( widgetId ) {
+ var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
+ if ( widgetPartial ) {
+ 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();
+ sidebarWidgets.push( {
+ partial: widgetPartial,
+ placement: widgetPlacement,
+ position: thisPosition
+ } );
+ if ( thisPosition < lastPosition ) {
+ needsSort = true;
+ }
+ lastPosition = thisPosition;
+ }
+ } );
+ } );
+
+ if ( needsSort ) {
+ _.each( sidebarWidgets, function( sidebarWidget ) {
+ sidebarPlacement.endNode.parentNode.insertBefore(
+ sidebarWidget.placement.container[0],
+ sidebarPlacement.endNode
+ );
+
+ // @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
+ * widget ID.
+ *
+ * @since 4.5.0
+ *
+ * @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 );
+ if ( ! widgetPartial ) {
+ widgetPartial = new self.WidgetPartial( partialId, {
+ params: {}
+ } );
+ }
+
+ // 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 ) {
+ return;
+ }
+
+ 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] ) {
+ return;
+ }
+
+ 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;
+ } );
+
+ api.selectiveRefresh.partial.add( widgetPartial );
+
+ if ( wasInserted ) {
+ sidebarPartial.reflowWidgets();
+ }
+
+ return widgetPartial;
+ },
+
+ /**
+ * Handles changes to the sidebars_widgets[] setting.
+ *
+ * @since 4.5.0
+ *
+ * @param {Array} newWidgetIds New widget IDs.
+ * @param {Array} oldWidgetIds Old widget IDs.
+ *
+ * @return {void}
+ */
+ 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();
+ }
+ } );
+ }
+ delete self.renderedWidgets[ removedWidgetId ];
+ } );
+
+ // Handle insertion of widgets.
+ widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
+ _.each( widgetsAdded, function( addedWidgetId ) {
+ var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
+ addedWidgetPartials.push( widgetPartial );
+ self.renderedWidgets[ addedWidgetId ] = true;
+ } );
+
+ _.each( addedWidgetPartials, function( widgetPartial ) {
+ widgetPartial.refresh();
+ } );
+
+ api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
+ },
+
+ /**
+ * Refreshes the sidebar partial.
+ *
+ * Note that the meat is handled in handleSettingChange because it has the
+ * context of which widgets were removed.
+ *
+ * @since 4.5.0
+ *
+ * @return {Promise} A promise postponing the refresh.
+ */
+ 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;
+
+ /**
+ * Adds partials for the registered widget areas (sidebars).
+ *
+ * @since 4.5.0
+ *
+ * @return {void}
+ */
+ 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 );
+ }
+ } );
+ };
+
+ /**
+ * Calculates the selector for the sidebar's widgets based on the registered
+ * sidebar's info.
+ *
+ * @memberOf wp.customize.widgetsPreview
+ *
+ * @since 3.9.0
+ *
+ * @return {void}
+ */
+ self.buildWidgetSelectors = function() {
+ var self = this;
+
+ $.each( self.registeredSidebars, function( i, sidebar ) {
+ var widgetTpl = [
+ sidebar.before_widget,
+ 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;
+ }
+
+ // Remove class names that incorporate the string formatting placeholders %1$s and %2$s.
+ widgetClasses = widgetClasses.replace( /\S*%[12]\$s\S*/g, '' );
+ widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
+ if ( widgetClasses ) {
+ widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
+ }
+ self.widgetSelectors.push( widgetSelector );
+ });
+ };
+
+ /**
+ * Highlights the widget on widget updates or widget control mouse overs.
+ *
+ * @memberOf wp.customize.widgetsPreview
+ *
+ * @since 3.9.0
+ * @param {string} widgetId ID of the widget.
+ *
+ * @return {void}
+ */
+ 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 );
+ };
+
+ /**
+ * Shows a title and highlights widgets on hover. On shift+clicking focuses the
+ * widget control.
+ *
+ * @memberOf wp.customize.widgetsPreview
+ *
+ * @since 3.9.0
+ *
+ * @return {void}
+ */
+ self.highlightControls = function() {
+ var self = this,
+ selector = this.widgetSelectors.join( ',' );
+
+ // Skip adding highlights if not in the customizer preview iframe.
+ if ( ! api.settings.channel ) {
+ return;
+ }
+
+ $( selector ).attr( 'title', this.l10n.widgetTooltip );
+ // Highlights widget when entering the widget editor.
+ $( 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' ) );
+ });
+ };
+
+ /**
+ * Parses a widget ID.
+ *
+ * @memberOf wp.customize.widgetsPreview
+ *
+ * @since 4.5.0
+ *
+ * @param {string} widgetId The widget ID.
+ *
+ * @return {{idBase: string, number: number|null}} An object containing the idBase
+ * and number of the parsed widget ID.
+ */
+ 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.
+ }
+
+ return parsed;
+ };
+
+ /**
+ * Parses a widget setting ID.
+ *
+ * @memberOf wp.customize.widgetsPreview
+ *
+ * @since 4.5.0
+ *
+ * @param {string} settingId Widget setting ID.
+ *
+ * @return {{idBase: string, number: number|null}|null} Either an object containing the idBase
+ * and number of the parsed widget setting ID,
+ * or null.
+ */
+ self.parseWidgetSettingId = function( settingId ) {
+ var matches, parsed = {
+ idBase: '',
+ number: null
+ };
+
+ matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
+ if ( ! matches ) {
+ return null;
+ }
+ parsed.idBase = matches[1];
+ if ( matches[2] ) {
+ parsed.number = parseInt( matches[2], 10 );
+ }
+ return parsed;
+ };
+
+ /**
+ * Converts a widget ID into a Customizer setting ID.
+ *
+ * @memberOf wp.customize.widgetsPreview
+ *
+ * @since 4.5.0
+ *
+ * @param {string} widgetId The widget ID.
+ *
+ * @return {string} The setting ID.
+ */
+ self.getWidgetSettingId = function( widgetId ) {
+ var parsed = this.parseWidgetId( widgetId ), settingId;
+
+ settingId = 'widget_' + parsed.idBase;
+ if ( parsed.number ) {
+ settingId += '[' + String( parsed.number ) + ']';
+ }
+
+ return settingId;
+ };
+
+ api.bind( 'preview-ready', function() {
+ $.extend( self, _wpWidgetCustomizerPreviewSettings );
+ self.init();
+ });
+
+ return self;
+})( jQuery, _, wp, wp.customize );