home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / js / customize-preview-widgets.js < prev    next >
Encoding:
JavaScript  |  2017-10-15  |  20.6 KB  |  714 lines

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