]> scripts.mit.edu Git - autoinstalls/wordpress.git/blobdiff - wp-includes/js/customize-preview-widgets.js
WordPress 4.5-scripts
[autoinstalls/wordpress.git] / wp-includes / js / customize-preview-widgets.js
index 92267e88fb3317d040d7b594009bba6cba7b8a48..b22087b64a2585fd6d2737fb092aa2a97858d9b5 100644 (file)
-(function( wp, $ ){
+/* global _wpWidgetCustomizerPreviewSettings */
+wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
 
-       if ( ! wp || ! wp.customize ) { return; }
+       var self;
 
-       var api = wp.customize,
-               OldPreview;
+       self = {
+               renderedSidebars: {},
+               renderedWidgets: {},
+               registeredSidebars: [],
+               registeredWidgets: {},
+               widgetSelectors: [],
+               preview: null,
+               l10n: {
+                       widgetTooltip: ''
+               },
+               selectiveRefreshableWidgets: {}
+       };
 
        /**
-        * wp.customize.WidgetCustomizerPreview
+        * Init widgets preview.
         *
+        * @since 4.5.0
         */
-       api.WidgetCustomizerPreview = {
-               renderedSidebars: {}, // @todo Make rendered a property of the Backbone model
-               renderedWidgets: {}, // @todo Make rendered a property of the Backbone model
-               registeredSidebars: [], // @todo Make a Backbone collection
-               registeredWidgets: {}, // @todo Make array, Backbone collection
-               widgetSelectors: [],
-               preview: null,
-               l10n: {},
+       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();
+               } );
+       };
+
+       /**
+        * Partial representing a widget instance.
+        *
+        * @class
+        * @augments wp.customize.selectiveRefresh.Partial
+        * @since 4.5.0
+        */
+       self.WidgetPartial = api.selectiveRefresh.Partial.extend({
+
+               /**
+                * Constructor.
+                *
+                * @since 4.5.0
+                * @param {string} id - Partial ID.
+                * @param {Object} options
+                * @param {Object} options.params
+                */
+               initialize: function( id, options ) {
+                       var partial = this, matches;
+                       matches = id.match( /^widget\[(.+)]$/ );
+                       if ( ! matches ) {
+                               throw new Error( 'Illegal id for widget partial.' );
+                       }
 
-               init: function () {
-                       var self = this;
-                       this.buildWidgetSelectors();
-                       this.highlightControls();
+                       partial.widgetId = matches[1];
+                       partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
+                       options = options || {};
+                       options.params = _.extend(
+                               {
+                                       settings: [ self.getWidgetSettingId( partial.widgetId ) ],
+                                       containerInclusive: true
+                               },
+                               options.params || {}
+                       );
 
-                       this.preview.bind( 'highlight-widget', self.highlightWidget );
+                       api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
                },
 
                /**
-                * Calculate the selector for the sidebar's widgets based on the registered sidebar's info
+                * Refresh widget partial.
+                *
+                * @returns {Promise}
                 */
-               buildWidgetSelectors: function () {
-                       var self = this;
-
-                       $.each( this.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;
-                               }
+               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 );
+                       }
+               },
+
+               /**
+                * Send widget-updated message to parent so spinner will get removed from widget control.
+                *
+                * @inheritdoc
+                * @param {wp.customize.selectiveRefresh.Placement} placement
+                */
+               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 );
+                       }
+               }
+       });
 
-                               widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, '');
+       /**
+        * Partial representing a widget area.
+        *
+        * @class
+        * @augments wp.customize.selectiveRefresh.Partial
+        * @since 4.5.0
+        */
+       self.SidebarPartial = api.selectiveRefresh.Partial.extend({
+
+               /**
+                * Constructor.
+                *
+                * @since 4.5.0
+                * @param {string} id - Partial ID.
+                * @param {Object} options
+                * @param {Object} options.params
+                */
+               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 || {}
+                       );
 
-                               if ( widgetClasses ) {
-                                       widgetSelector += '.' + widgetClasses.split(/\s+/).join('.');
+                       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' );
+                       }
+               },
+
+               /**
+                * Set up the partial.
+                *
+                * @since 4.5.0
+                */
+               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 );
                                }
-                               self.widgetSelectors.push(widgetSelector);
-                       });
+                               if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
+                                       sidebarPartial.ensureWidgetPlacementContainers( widgetId );
+                               }
+                       } );
                },
 
                /**
-                * Highlight the widget on widget updates or widget control mouse overs.
+                * Get the before/after boundary nodes for all instances of this sidebar (usually one).
+                *
+                * Note that TreeWalker is not implemented in IE8.
                 *
-                * @param  {string} widgetId ID of the widget.
+                * @since 4.5.0
+                * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
                 */
-               highlightWidget: function( widgetId ) {
-                       var $body = $( document.body ),
-                               $widget = $( '#' + widgetId );
+               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 );
+                                       }
+                               } );
+                       };
 
-                       $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
+                       recursiveCommentTraversal( document.body.childNodes );
+                       return _.values( boundaryNodes );
+               },
 
-                       $widget.addClass( 'widget-customizer-highlighted-widget' );
-                       setTimeout( function () {
-                               $widget.removeClass( 'widget-customizer-highlighted-widget' );
-                       }, 500 );
+               /**
+                * Get the placements for this partial.
+                *
+                * @since 4.5.0
+                * @returns {Array}
+                */
+               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
+                                       }
+                               } );
+                       } );
                },
 
                /**
-                * Show a title and highlight widgets on hover. On shift+clicking
-                * focus the widget control.
+                * Get the list of widget IDs associated with this widget area.
+                *
+                * @since 4.5.0
+                *
+                * @returns {Array}
                 */
-               highlightControls: function() {
-                       var self = this,
-                               selector = this.widgetSelectors.join(',');
+               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 );
+               },
 
-                       $(selector).attr( 'title', this.l10n.widgetTooltip );
+               /**
+                * Reflow 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 );
+                                       } );
 
-                       $(document).on( 'mouseenter', selector, function () {
-                               self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
-                       });
+                                       sortedSidebarContainers.push( sidebarPlacement );
+                               }
+                       } );
+
+                       if ( sortedSidebarContainers.length > 0 ) {
+                               api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
+                       }
+
+                       return sortedSidebarContainers;
+               },
+
+               /**
+                * Make sure there is a widget instance container in this sidebar for the given widget ID.
+                *
+                * @since 4.5.0
+                *
+                * @param {string} widgetId
+                * @returns {wp.customize.selectiveRefresh.Partial} 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: {}
+                               } );
+                               api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
+                       }
 
-                       // Open expand the widget control when shift+clicking the widget element
-                       $(document).on( 'click', selector, function ( e ) {
-                               if ( ! e.shiftKey ) {
+                       // 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;
                                }
-                               e.preventDefault();
 
-                               self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
-                       });
+                               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.
                }
+
+               return parsed;
        };
 
        /**
-        * Capture the instance of the Preview since it is private
+        * Parse a widget setting ID.
+        *
+        * @since 4.5.0
+        *
+        * @param {string} settingId Widget setting ID.
+        * @returns {{idBase: string, number: number|null}|null}
         */
-       OldPreview = api.Preview;
-       api.Preview = OldPreview.extend( {
-               initialize: function( params, options ) {
-                       api.WidgetCustomizerPreview.preview = this;
-                       OldPreview.prototype.initialize.call( this, params, options );
+       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;
+       };
 
-       $(function () {
-               var settings = window._wpWidgetCustomizerPreviewSettings;
-               if ( ! settings ) {
-                       return;
+       /**
+        * Convert a widget ID into a Customizer setting ID.
+        *
+        * @since 4.5.0
+        *
+        * @param {string} widgetId Widget ID.
+        * @returns {string} settingId Setting ID.
+        */
+       self.getWidgetSettingId = function( widgetId ) {
+               var parsed = this.parseWidgetId( widgetId ), settingId;
+
+               settingId = 'widget_' + parsed.idBase;
+               if ( parsed.number ) {
+                       settingId += '[' + String( parsed.number ) + ']';
                }
 
-               $.extend( api.WidgetCustomizerPreview, settings );
+               return settingId;
+       };
 
-               api.WidgetCustomizerPreview.init();
+       api.bind( 'preview-ready', function() {
+               $.extend( self, _wpWidgetCustomizerPreviewSettings );
+               self.init();
        });
 
-})( window.wp, jQuery );
+       return self;
+})( jQuery, _, wp, wp.customize );