[ 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, sidebarArgs, 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              // Build containers from the registered sidebar, since partial params can be declared in post content.
 424              sidebarArgs = _.findWhere( self.registeredSidebars, { id: sidebarPartial.sidebarId } );
 425  
 426              // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
 427              _.each( sidebarPartial.placements(), function( sidebarPlacement ) {
 428                  var foundWidgetPlacement, widgetContainerElement;
 429  
 430                  if ( ! sidebarArgs ) {
 431                      return;
 432                  }
 433  
 434                  foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
 435                      return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
 436                  } );
 437                  if ( foundWidgetPlacement ) {
 438                      return;
 439                  }
 440  
 441                  widgetContainerElement = $(
 442                      sidebarArgs.before_widget.replace( /%1\$s/g, widgetId ).replace( /%2\$s/g, 'widget' ) +
 443                      sidebarArgs.after_widget
 444                  );
 445  
 446                  // Handle rare case where before_widget and after_widget are empty.
 447                  if ( ! widgetContainerElement[0] ) {
 448                      return;
 449                  }
 450  
 451                  widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
 452                  widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
 453                  widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
 454  
 455                  /*
 456                   * Make sure the widget container element has the customize-container context data.
 457                   * The sidebar_instance_number is used to disambiguate multiple instances of the
 458                   * same sidebar are rendered onto the template, and so the same widget is embedded
 459                   * multiple times.
 460                   */
 461                  widgetContainerElement.data( 'customize-partial-placement-context', {
 462                      'sidebar_id': sidebarPartial.sidebarId,
 463                      'sidebar_instance_number': sidebarPlacement.context.instanceNumber
 464                  } );
 465  
 466                  sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
 467                  wasInserted = true;
 468              } );
 469  
 470              api.selectiveRefresh.partial.add( widgetPartial );
 471  
 472              if ( wasInserted ) {
 473                  sidebarPartial.reflowWidgets();
 474              }
 475  
 476              return widgetPartial;
 477          },
 478  
 479          /**
 480           * Handles changes to the sidebars_widgets[] setting.
 481           *
 482           * @since 4.5.0
 483           *
 484           * @param {string[]} newWidgetIds New widget IDs.
 485           * @param {string[]} oldWidgetIds Old widget IDs.
 486           *
 487           * @return {void}
 488           */
 489          handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
 490              var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
 491  
 492              needsRefresh = (
 493                  ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
 494                  ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
 495              );
 496              if ( needsRefresh ) {
 497                  sidebarPartial.fallback();
 498                  return;
 499              }
 500  
 501              // Handle removal of widgets.
 502              widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
 503              _.each( widgetsRemoved, function( removedWidgetId ) {
 504                  var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
 505                  if ( widgetPartial ) {
 506                      _.each( widgetPartial.placements(), function( placement ) {
 507                          var isRemoved = (
 508                              placement.context.sidebar_id === sidebarPartial.sidebarId ||
 509                              ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
 510                          );
 511                          if ( isRemoved ) {
 512                              placement.container.remove();
 513                          }
 514                      } );
 515                  }
 516                  delete self.renderedWidgets[ removedWidgetId ];
 517              } );
 518  
 519              // Handle insertion of widgets.
 520              widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
 521              _.each( widgetsAdded, function( addedWidgetId ) {
 522                  var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
 523                  addedWidgetPartials.push( widgetPartial );
 524                  self.renderedWidgets[ addedWidgetId ] = true;
 525              } );
 526  
 527              _.each( addedWidgetPartials, function( widgetPartial ) {
 528                  widgetPartial.refresh();
 529              } );
 530  
 531              api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
 532          },
 533  
 534          /**
 535           * Refreshes the sidebar partial.
 536           *
 537           * Note that the meat is handled in handleSettingChange because it has the
 538           * context of which widgets were removed.
 539           *
 540           * @since 4.5.0
 541           *
 542           * @return {JQuery.Promise<*>} Promise resolved once the sidebar's widgets have been reflowed, or rejected when the sidebar has no placements.
 543           */
 544          refresh: function() {
 545              var partial = this, deferred = $.Deferred();
 546  
 547              deferred.fail( function() {
 548                  partial.fallback();
 549              } );
 550  
 551              if ( 0 === partial.placements().length ) {
 552                  deferred.reject();
 553              } else {
 554                  _.each( partial.reflowWidgets(), function( sidebarPlacement ) {
 555                      api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
 556                  } );
 557                  deferred.resolve();
 558              }
 559  
 560              return deferred.promise();
 561          }
 562      });
 563  
 564      api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
 565      api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
 566  
 567      /**
 568       * Adds partials for the registered widget areas (sidebars).
 569       *
 570       * @since 4.5.0
 571       *
 572       * @return {void}
 573       */
 574      self.addPartials = function() {
 575          _.each( self.registeredSidebars, function( registeredSidebar ) {
 576              var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
 577              partial = api.selectiveRefresh.partial( partialId );
 578              if ( ! partial ) {
 579                  partial = new self.SidebarPartial( partialId, {
 580                      params: {
 581                          sidebarArgs: registeredSidebar
 582                      }
 583                  } );
 584                  api.selectiveRefresh.partial.add( partial );
 585              }
 586          } );
 587      };
 588  
 589      /**
 590       * Calculates the selector for the sidebar's widgets based on the registered
 591       * sidebar's info.
 592       *
 593       * @memberOf wp.customize.widgetsPreview
 594       *
 595       * @since 3.9.0
 596       *
 597       * @return {void}
 598       */
 599      self.buildWidgetSelectors = function() {
 600          var self = this;
 601  
 602          $.each( self.registeredSidebars, function( i, sidebar ) {
 603              var widgetTpl = [
 604                      sidebar.before_widget,
 605                      sidebar.before_title,
 606                      sidebar.after_title,
 607                      sidebar.after_widget
 608                  ].join( '' ),
 609                  emptyWidget,
 610                  widgetSelector,
 611                  widgetClasses;
 612  
 613              emptyWidget = $( widgetTpl );
 614              widgetSelector = emptyWidget.prop( 'tagName' ) || '';
 615              widgetClasses = emptyWidget.prop( 'className' ) || '';
 616  
 617              // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
 618              if ( ! widgetClasses ) {
 619                  return;
 620              }
 621  
 622              // Remove class names that incorporate the string formatting placeholders %1$s and %2$s.
 623              widgetClasses = widgetClasses.replace( /\S*%[12]\$s\S*/g, '' );
 624              widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
 625              if ( widgetClasses ) {
 626                  widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
 627              }
 628              self.widgetSelectors.push( widgetSelector );
 629          });
 630      };
 631  
 632      /**
 633       * Highlights the widget on widget updates or widget control mouse overs.
 634       *
 635       * @memberOf wp.customize.widgetsPreview
 636       *
 637       * @since 3.9.0
 638       * @param {string} widgetId ID of the widget.
 639       *
 640       * @return {void}
 641       */
 642      self.highlightWidget = function( widgetId ) {
 643          var $body = $( document.body ),
 644              $widget = $( '#' + widgetId );
 645  
 646          $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
 647  
 648          $widget.addClass( 'widget-customizer-highlighted-widget' );
 649          setTimeout( function() {
 650              $widget.removeClass( 'widget-customizer-highlighted-widget' );
 651          }, 500 );
 652      };
 653  
 654      /**
 655       * Shows a title and highlights widgets on hover. On shift+clicking focuses the
 656       * widget control.
 657       *
 658       * @memberOf wp.customize.widgetsPreview
 659       *
 660       * @since 3.9.0
 661       *
 662       * @return {void}
 663       */
 664      self.highlightControls = function() {
 665          var self = this,
 666              selector = this.widgetSelectors.join( ',' );
 667  
 668          // Skip adding highlights if not in the customizer preview iframe.
 669          if ( ! api.settings.channel ) {
 670              return;
 671          }
 672  
 673          $( selector ).attr( 'title', this.l10n.widgetTooltip );
 674          // Highlights widget when entering the widget editor.
 675          $( document ).on( 'mouseenter', selector, function() {
 676              self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
 677          });
 678  
 679          // Open expand the widget control when shift+clicking the widget element.
 680          $( document ).on( 'click', selector, function( e ) {
 681              if ( ! e.shiftKey ) {
 682                  return;
 683              }
 684              e.preventDefault();
 685  
 686              self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
 687          });
 688      };
 689  
 690      /**
 691       * Parses a widget ID.
 692       *
 693       * @memberOf wp.customize.widgetsPreview
 694       *
 695       * @since 4.5.0
 696       *
 697       * @param {string} widgetId The widget ID.
 698       *
 699       * @return {{idBase: string, number: number|null}} An object containing the idBase
 700       *                                                 and number of the parsed widget ID.
 701       */
 702      self.parseWidgetId = function( widgetId ) {
 703          var matches, parsed = {
 704              idBase: '',
 705              number: null
 706          };
 707  
 708          matches = widgetId.match( /^(.+)-(\d+)$/ );
 709          if ( matches ) {
 710              parsed.idBase = matches[1];
 711              parsed.number = parseInt( matches[2], 10 );
 712          } else {
 713              parsed.idBase = widgetId; // Likely an old single widget.
 714          }
 715  
 716          return parsed;
 717      };
 718  
 719      /**
 720       * Parses a widget setting ID.
 721       *
 722       * @memberOf wp.customize.widgetsPreview
 723       *
 724       * @since 4.5.0
 725       *
 726       * @param {string} settingId Widget setting ID.
 727       *
 728       * @return {{idBase: string, number: number|null}|null} Either an object containing the idBase
 729       *                                                      and number of the parsed widget setting ID,
 730       *                                                      or null.
 731       */
 732      self.parseWidgetSettingId = function( settingId ) {
 733          var matches, parsed = {
 734              idBase: '',
 735              number: null
 736          };
 737  
 738          matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
 739          if ( ! matches ) {
 740              return null;
 741          }
 742          parsed.idBase = matches[1];
 743          if ( matches[2] ) {
 744              parsed.number = parseInt( matches[2], 10 );
 745          }
 746          return parsed;
 747      };
 748  
 749      /**
 750       * Converts a widget ID into a Customizer setting ID.
 751       *
 752       * @memberOf wp.customize.widgetsPreview
 753       *
 754       * @since 4.5.0
 755       *
 756       * @param {string} widgetId The widget ID.
 757       *
 758       * @return {string} The setting ID.
 759       */
 760      self.getWidgetSettingId = function( widgetId ) {
 761          var parsed = this.parseWidgetId( widgetId ), settingId;
 762  
 763          settingId = 'widget_' + parsed.idBase;
 764          if ( parsed.number ) {
 765              settingId += '[' + String( parsed.number ) + ']';
 766          }
 767  
 768          return settingId;
 769      };
 770  
 771      api.bind( 'preview-ready', function() {
 772          $.extend( self, _wpWidgetCustomizerPreviewSettings );
 773          self.init();
 774      });
 775  
 776      return self;
 777  })( jQuery, _, wp, wp.customize );


Generated : Mon Nov 9 08:20:01 2026 Cross-referenced by PHPXref