[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/js/ -> customize-preview-widgets.js (source)

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


Generated : Wed Sep 9 08:20:27 2026 Cross-referenced by PHPXref