[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/js/ -> customize-selective-refresh.js (source)

   1  /**
   2   * @output wp-includes/js/customize-selective-refresh.js
   3   */
   4  
   5  /* global jQuery, JSON, _customizePartialRefreshExports, console */
   6  
   7  /** @namespace wp.customize.selectiveRefresh */
   8  
   9  /**
  10   * @param {JQueryStatic} $   The jQuery object.
  11   * @param {Object}       api The Customizer API.
  12   */
  13  wp.customize.selectiveRefresh = ( function( $, api ) {
  14      'use strict';
  15      var self, Partial, Placement;
  16  
  17      self = {
  18          ready: $.Deferred(),
  19          editShortcutVisibility: new api.Value(),
  20          data: {
  21              partials: {},
  22              renderQueryVar: '',
  23              l10n: {
  24                  shiftClickToEdit: ''
  25              }
  26          },
  27          currentRequest: null
  28      };
  29  
  30      _.extend( self, api.Events );
  31  
  32      /**
  33       * A Customizer Partial.
  34       *
  35       * A partial provides a rendering of one or more settings according to a template.
  36       *
  37       * @memberOf wp.customize.selectiveRefresh
  38       * @alias wp.customize.selectiveRefresh.Partial
  39       *
  40       * @see PHP class WP_Customize_Partial.
  41       *
  42       * @class
  43       * @augments wp.customize.Class
  44       * @since 4.5.0
  45       */
  46      Partial = self.Partial = api.Class.extend(/** @lends wp.customize.selectiveRefresh.Partial.prototype */{
  47  
  48          id: null,
  49  
  50          /**
  51           * Default params.
  52           *
  53           * @since 4.9.0
  54           * @member {Object}
  55           */
  56          defaults: {
  57              selector: null,
  58              primarySetting: null,
  59              containerInclusive: false,
  60              fallbackRefresh: true // Note this needs to be false in a front-end editing context.
  61          },
  62  
  63          /**
  64           * Constructor.
  65           *
  66           * @since 4.5.0
  67           *
  68           * @param {string}   id                        Unique identifier for the partial instance.
  69           * @param {Object}   [options]                 Options hash for the partial instance.
  70           * @param {string}   [options.type]            Type of partial (e.g. nav_menu, widget, etc.).
  71           * @param {string}   [options.selector]        jQuery selector to find the container element in the page.
  72           * @param {string[]} [options.settings]        The IDs for the settings the partial relates to.
  73           * @param {string}   [options.primarySetting]  The ID for the primary setting the partial renders.
  74           * @param {boolean}  [options.fallbackRefresh] Whether to refresh the entire preview in case of a partial refresh failure.
  75           * @param {Object}   [options.params]          Deprecated wrapper for the above properties.
  76           */
  77          initialize: function( id, options ) {
  78              var partial = this;
  79              options = options || {};
  80              partial.id = id;
  81  
  82              partial.params = _.extend(
  83                  {
  84                      settings: []
  85                  },
  86                  partial.defaults,
  87                  options.params || options
  88              );
  89  
  90              partial.deferred = {};
  91              partial.deferred.ready = $.Deferred();
  92  
  93              partial.deferred.ready.done( function() {
  94                  partial.ready();
  95              } );
  96          },
  97  
  98          /**
  99           * Set up the partial.
 100           *
 101           * @since 4.5.0
 102           */
 103          ready: function() {
 104              var partial = this;
 105              _.each( partial.placements(), function( placement ) {
 106                  $( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit );
 107                  partial.createEditShortcutForPlacement( placement );
 108              } );
 109              $( document ).on( 'click', partial.params.selector, function( e ) {
 110                  if ( ! e.shiftKey ) {
 111                      return;
 112                  }
 113                  e.preventDefault();
 114                  _.each( partial.placements(), function( placement ) {
 115                      if ( $( placement.container ).is( e.currentTarget ) ) {
 116                          partial.showControl();
 117                      }
 118                  } );
 119              } );
 120          },
 121  
 122          /**
 123           * Create and show the edit shortcut for a given partial placement container.
 124           *
 125           * @since 4.7.0
 126           * @access public
 127           *
 128           * @param {wp.customize.selectiveRefresh.Placement} placement The placement container element.
 129           * @return {void}
 130           */
 131          createEditShortcutForPlacement: function( placement ) {
 132              var partial = this, $shortcut, $placementContainer, illegalAncestorSelector, illegalContainerSelector;
 133              if ( ! placement.container ) {
 134                  return;
 135              }
 136              $placementContainer = $( placement.container );
 137              illegalAncestorSelector = 'head';
 138              illegalContainerSelector = 'area, audio, base, bdi, bdo, br, button, canvas, col, colgroup, command, datalist, embed, head, hr, html, iframe, img, input, keygen, label, link, map, math, menu, meta, noscript, object, optgroup, option, param, progress, rp, rt, ruby, script, select, source, style, svg, table, tbody, textarea, tfoot, thead, title, tr, track, video, wbr';
 139              if ( ! $placementContainer.length || $placementContainer.is( illegalContainerSelector ) || $placementContainer.closest( illegalAncestorSelector ).length ) {
 140                  return;
 141              }
 142              $shortcut = partial.createEditShortcut();
 143              $shortcut.on( 'click', function( event ) {
 144                  event.preventDefault();
 145                  event.stopPropagation();
 146                  partial.showControl();
 147              } );
 148              partial.addEditShortcutToPlacement( placement, $shortcut );
 149          },
 150  
 151          /**
 152           * Add an edit shortcut to the placement container.
 153           *
 154           * @since 4.7.0
 155           * @access public
 156           *
 157           * @param {wp.customize.selectiveRefresh.Placement} placement     The placement for the partial.
 158           * @param {JQuery}                                  $editShortcut The shortcut element as a jQuery object.
 159           * @return {void}
 160           */
 161          addEditShortcutToPlacement: function( placement, $editShortcut ) {
 162              var $placementContainer = $( placement.container );
 163              $placementContainer.prepend( $editShortcut );
 164              if ( ! $placementContainer.is( ':visible' ) || 'none' === $placementContainer.css( 'display' ) ) {
 165                  $editShortcut.addClass( 'customize-partial-edit-shortcut-hidden' );
 166              }
 167          },
 168  
 169          /**
 170           * Return the unique class name for the edit shortcut button for this partial.
 171           *
 172           * @since 4.7.0
 173           * @access public
 174           *
 175           * @return {string} Partial ID converted into a class name for use in shortcut.
 176           */
 177          getEditShortcutClassName: function() {
 178              var partial = this, cleanId;
 179              cleanId = partial.id.replace( /]/g, '' ).replace( /\[/g, '-' );
 180              return 'customize-partial-edit-shortcut-' + cleanId;
 181          },
 182  
 183          /**
 184           * Return the appropriate translated string for the edit shortcut button.
 185           *
 186           * @since 4.7.0
 187           * @access public
 188           *
 189           * @return {string} Tooltip for edit shortcut.
 190           */
 191          getEditShortcutTitle: function() {
 192              var partial = this, l10n = self.data.l10n;
 193              switch ( partial.getType() ) {
 194                  case 'widget':
 195                      return l10n.clickEditWidget;
 196                  case 'blogname':
 197                      return l10n.clickEditTitle;
 198                  case 'blogdescription':
 199                      return l10n.clickEditTitle;
 200                  case 'nav_menu':
 201                      return l10n.clickEditMenu;
 202                  default:
 203                      return l10n.clickEditMisc;
 204              }
 205          },
 206  
 207          /**
 208           * Return the type of this partial
 209           *
 210           * Will use `params.type` if set, but otherwise will try to infer type from settingId.
 211           *
 212           * @since 4.7.0
 213           * @access public
 214           *
 215           * @return {string} Type of partial derived from type param or the related setting ID.
 216           */
 217          getType: function() {
 218              var partial = this, settingId;
 219              settingId = partial.params.primarySetting || _.first( partial.settings() ) || 'unknown';
 220              if ( partial.params.type ) {
 221                  return partial.params.type;
 222              }
 223              if ( settingId.match( /^nav_menu_instance\[/ ) ) {
 224                  return 'nav_menu';
 225              }
 226              if ( settingId.match( /^widget_.+\[\d+]$/ ) ) {
 227                  return 'widget';
 228              }
 229              return settingId;
 230          },
 231  
 232          /**
 233           * Create an edit shortcut button for this partial.
 234           *
 235           * @since 4.7.0
 236           * @access public
 237           *
 238           * @return {JQuery} The edit shortcut button element.
 239           */
 240          createEditShortcut: function() {
 241              var partial = this, shortcutTitle, $buttonContainer, $button, $image;
 242              shortcutTitle = partial.getEditShortcutTitle();
 243              $buttonContainer = $( '<span>', {
 244                  'class': 'customize-partial-edit-shortcut ' + partial.getEditShortcutClassName()
 245              } );
 246              $button = $( '<button>', {
 247                  'aria-label': shortcutTitle,
 248                  'title': shortcutTitle,
 249                  'class': 'customize-partial-edit-shortcut-button'
 250              } );
 251              $image = $( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13.89 3.39l2.71 2.72c.46.46.42 1.24.03 1.64l-8.01 8.02-5.56 1.16 1.16-5.58s7.6-7.63 7.99-8.03c.39-.39 1.22-.39 1.68.07zm-2.73 2.79l-5.59 5.61 1.11 1.11 5.54-5.65zm-2.97 8.23l5.58-5.6-1.07-1.08-5.59 5.6z"/></svg>' );
 252              $button.append( $image );
 253              $buttonContainer.append( $button );
 254              return $buttonContainer;
 255          },
 256  
 257          /**
 258           * Find all placements for this partial in the document.
 259           *
 260           * @since 4.5.0
 261           *
 262           * @return {wp.customize.selectiveRefresh.Placement[]} The placements for this partial in the document.
 263           */
 264          placements: function() {
 265              var partial = this, selector;
 266  
 267              selector = partial.params.selector || '';
 268              if ( selector ) {
 269                  selector += ', ';
 270              }
 271              selector += '[data-customize-partial-id="' + partial.id + '"]'; // @todo Consider injecting customize-partial-id-${id} classnames instead.
 272  
 273              return $( selector ).map( function() {
 274                  var container = $( this ), context;
 275  
 276                  context = container.data( 'customize-partial-placement-context' );
 277                  if ( _.isString( context ) && '{' === context.substr( 0, 1 ) ) {
 278                      throw new Error( 'context JSON parse error' );
 279                  }
 280  
 281                  return new Placement( {
 282                      partial: partial,
 283                      container: container,
 284                      context: context
 285                  } );
 286              } ).get();
 287          },
 288  
 289          /**
 290           * Get list of setting IDs related to this partial.
 291           *
 292           * @since 4.5.0
 293           *
 294           * @return {string[]} The setting IDs related to this partial.
 295           */
 296          settings: function() {
 297              var partial = this;
 298              if ( partial.params.settings && 0 !== partial.params.settings.length ) {
 299                  return partial.params.settings;
 300              } else if ( partial.params.primarySetting ) {
 301                  return [ partial.params.primarySetting ];
 302              } else {
 303                  return [ partial.id ];
 304              }
 305          },
 306  
 307          /**
 308           * Return whether the setting is related to the partial.
 309           *
 310           * @since 4.5.0
 311           *
 312           * @param {wp.customize.Value|string} setting ID or object for setting.
 313           * @return {boolean} Whether the setting is related to the partial.
 314           */
 315          isRelatedSetting: function( setting /*... newValue, oldValue */ ) {
 316              var partial = this;
 317              if ( _.isString( setting ) ) {
 318                  setting = api( setting );
 319              }
 320              if ( ! setting ) {
 321                  return false;
 322              }
 323              return -1 !== _.indexOf( partial.settings(), setting.id );
 324          },
 325  
 326          /**
 327           * Show the control to modify this partial's setting(s).
 328           *
 329           * This may be overridden for inline editing.
 330           *
 331           * @since 4.5.0
 332           */
 333          showControl: function() {
 334              var partial = this, settingId = partial.params.primarySetting;
 335              if ( ! settingId ) {
 336                  settingId = _.first( partial.settings() );
 337              }
 338              if ( partial.getType() === 'nav_menu' ) {
 339                  if ( partial.params.navMenuArgs.theme_location ) {
 340                      settingId = 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']';
 341                  } else if ( partial.params.navMenuArgs.menu )   {
 342                      settingId = 'nav_menu[' + String( partial.params.navMenuArgs.menu ) + ']';
 343                  }
 344              }
 345              api.preview.send( 'focus-control-for-setting', settingId );
 346          },
 347  
 348          /**
 349           * Prepare container for selective refresh.
 350           *
 351           * @since 4.5.0
 352           *
 353           * @param {wp.customize.selectiveRefresh.Placement} placement The placement to prepare.
 354           */
 355          preparePlacement: function( placement ) {
 356              $( placement.container ).addClass( 'customize-partial-refreshing' );
 357          },
 358  
 359          /**
 360           * Reference to the pending promise returned from self.requestPartial().
 361           *
 362           * @since 4.5.0
 363           * @private
 364           */
 365          _pendingRefreshPromise: null,
 366  
 367          /**
 368           * Request the new partial and render it into the placements.
 369           *
 370           * @since 4.5.0
 371           *
 372           * @this {wp.customize.selectiveRefresh.Partial}
 373           * @return {JQuery.Promise<*>} Promise for the request to render the partial.
 374           */
 375          refresh: function() {
 376              var partial = this, refreshPromise;
 377  
 378              refreshPromise = self.requestPartial( partial );
 379  
 380              if ( ! partial._pendingRefreshPromise ) {
 381                  _.each( partial.placements(), function( placement ) {
 382                      partial.preparePlacement( placement );
 383                  } );
 384  
 385                  refreshPromise.done( function( placements ) {
 386                      _.each( placements, function( placement ) {
 387                          partial.renderContent( placement );
 388                      } );
 389                  } );
 390  
 391                  refreshPromise.fail( function( data, placements ) {
 392                      partial.fallback( data, placements );
 393                  } );
 394  
 395                  // Allow new request when this one finishes.
 396                  partial._pendingRefreshPromise = refreshPromise;
 397                  refreshPromise.always( function() {
 398                      partial._pendingRefreshPromise = null;
 399                  } );
 400              }
 401  
 402              return refreshPromise;
 403          },
 404  
 405          /**
 406           * Apply the addedContent in the placement to the document.
 407           *
 408           * Note the placement object will have its container and removedNodes
 409           * properties updated.
 410           *
 411           * @since 4.5.0
 412           *
 413           * @param {wp.customize.selectiveRefresh.Placement} placement              The placement to render into.
 414           * @param {Element|JQuery}                          [placement.container]  This param will be empty if there was no element matching the selector.
 415           * @param {string|Object|boolean}                   placement.addedContent Rendered HTML content, a data object for JS templates to render, or false if no render.
 416           * @param {Object}                                  [placement.context]    Optional context information about the container.
 417           * @return {boolean} Whether the rendering was successful and the fallback was not invoked.
 418           */
 419          renderContent: function( placement ) {
 420              var partial = this, content, newContainerElement;
 421              if ( ! placement.container ) {
 422                  partial.fallback( new Error( 'no_container' ), [ placement ] );
 423                  return false;
 424              }
 425              placement.container = $( placement.container );
 426              if ( false === placement.addedContent ) {
 427                  partial.fallback( new Error( 'missing_render' ), [ placement ] );
 428                  return false;
 429              }
 430  
 431              // Currently a subclass needs to override renderContent to handle partials returning data object.
 432              if ( ! _.isString( placement.addedContent ) ) {
 433                  partial.fallback( new Error( 'non_string_content' ), [ placement ] );
 434                  return false;
 435              }
 436  
 437              /* jshint ignore:start */
 438              self.originalDocumentWrite = document.write;
 439              document.write = function() {
 440                  throw new Error( self.data.l10n.badDocumentWrite );
 441              };
 442              /* jshint ignore:end */
 443              try {
 444                  content = placement.addedContent;
 445                  if ( wp.emoji && wp.emoji.parse && ! $.contains( document.head, placement.container[0] ) ) {
 446                      content = wp.emoji.parse( content );
 447                  }
 448  
 449                  if ( partial.params.containerInclusive ) {
 450  
 451                      // Note that content may be an empty string, and in this case jQuery will just remove the oldContainer.
 452                      newContainerElement = $( content );
 453  
 454                      // Merge the new context on top of the old context.
 455                      placement.context = _.extend(
 456                          placement.context,
 457                          newContainerElement.data( 'customize-partial-placement-context' ) || {}
 458                      );
 459                      newContainerElement.data( 'customize-partial-placement-context', placement.context );
 460  
 461                      placement.removedNodes = placement.container;
 462                      placement.container = newContainerElement;
 463                      placement.removedNodes.replaceWith( placement.container );
 464                      placement.container.attr( 'title', self.data.l10n.shiftClickToEdit );
 465                  } else {
 466                      placement.removedNodes = document.createDocumentFragment();
 467                      while ( placement.container[0].firstChild ) {
 468                          placement.removedNodes.appendChild( placement.container[0].firstChild );
 469                      }
 470  
 471                      placement.container.html( content );
 472                  }
 473  
 474                  placement.container.removeClass( 'customize-render-content-error' );
 475              } catch ( error ) {
 476                  if ( 'undefined' !== typeof console && console.error ) {
 477                      console.error( partial.id, error );
 478                  }
 479                  partial.fallback( error, [ placement ] );
 480              }
 481              /* jshint ignore:start */
 482              document.write = self.originalDocumentWrite;
 483              self.originalDocumentWrite = null;
 484              /* jshint ignore:end */
 485  
 486              partial.createEditShortcutForPlacement( placement );
 487              placement.container.removeClass( 'customize-partial-refreshing' );
 488  
 489              // Prevent placement container from being re-triggered as being rendered among nested partials.
 490              placement.container.data( 'customize-partial-content-rendered', true );
 491  
 492              /*
 493               * Note that the 'wp_audio_shortcode_library' and 'wp_video_shortcode_library' filters
 494               * will determine whether or not wp.mediaelement is loaded and whether it will
 495               * initialize audio and video respectively. See also https://core.trac.wordpress.org/ticket/40144
 496               */
 497              if ( wp.mediaelement ) {
 498                  wp.mediaelement.initialize();
 499              }
 500  
 501              if ( wp.playlist ) {
 502                  wp.playlist.initialize();
 503              }
 504  
 505              /**
 506               * Announce when a partial's placement has been rendered so that dynamic elements can be re-built.
 507               */
 508              self.trigger( 'partial-content-rendered', placement );
 509              return true;
 510          },
 511  
 512          /**
 513           * Handle fail to render partial.
 514           *
 515           * The first argument is either the failing jqXHR or an Error object, and the second argument is the array of containers.
 516           *
 517           * @since 4.5.0
 518           */
 519          fallback: function() {
 520              var partial = this;
 521              if ( partial.params.fallbackRefresh ) {
 522                  self.requestFullRefresh();
 523              }
 524          }
 525      } );
 526  
 527      /**
 528       * A Placement for a Partial.
 529       *
 530       * A partial placement is the actual physical representation of a partial for a given context.
 531       * It also may have information in relation to how a placement may have just changed.
 532       * The placement is conceptually similar to a DOM Range or MutationRecord.
 533       *
 534       * @memberOf wp.customize.selectiveRefresh
 535       * @alias wp.customize.selectiveRefresh.Placement
 536       *
 537       * @class
 538       * @augments wp.customize.Class
 539       * @since 4.5.0
 540       */
 541      self.Placement = Placement = api.Class.extend(/** @lends wp.customize.selectiveRefresh.Placement.prototype */{
 542  
 543          /**
 544           * The partial with which the container is associated.
 545           *
 546           * @member {wp.customize.selectiveRefresh.Partial}
 547           */
 548          partial: null,
 549  
 550          /**
 551           * DOM element which contains the placement's contents.
 552           *
 553           * This will be null if the startNode and endNode do not point to the same
 554           * DOM element, such as in the case of a sidebar partial.
 555           * This container element itself will be replaced for partials that
 556           * have containerInclusive param defined as true.
 557           *
 558           * @member {JQuery}
 559           */
 560          container: null,
 561  
 562          /**
 563           * DOM node for the initial boundary of the placement.
 564           *
 565           * This will normally be the same as endNode since most placements appear as elements.
 566           * This is primarily useful for widget sidebars which do not have intrinsic containers, but
 567           * for which an HTML comment is output before to mark the starting position.
 568           *
 569           * @member {Node}
 570           */
 571          startNode: null,
 572  
 573          /**
 574           * DOM node for the terminal boundary of the placement.
 575           *
 576           * This will normally be the same as startNode since most placements appear as elements.
 577           * This is primarily useful for widget sidebars which do not have intrinsic containers, but
 578           * for which an HTML comment is output before to mark the ending position.
 579           *
 580           * @member {Node}
 581           */
 582          endNode: null,
 583  
 584          /**
 585           * Context data.
 586           *
 587           * This provides information about the placement which is included in the request
 588           * in order to render the partial properly.
 589           *
 590           * @member {Object}
 591           */
 592          context: null,
 593  
 594          /**
 595           * The content for the partial when refreshed.
 596           *
 597           * @member {string|Object|boolean}
 598           */
 599          addedContent: null,
 600  
 601          /**
 602           * DOM node(s) removed when the partial is refreshed.
 603           *
 604           * If the partial is containerInclusive, then the removedNodes will be
 605           * the jQuery object for the partial's former placement. If the
 606           * partial is not containerInclusive, then the removedNodes will be a
 607           * DocumentFragment containing the nodes removed.
 608           *
 609           * @member {JQuery|DocumentFragment}
 610           */
 611          removedNodes: null,
 612  
 613          /**
 614           * Constructor.
 615           *
 616           * @since 4.5.0
 617           *
 618           * @param {Object}                                args                The placement properties.
 619           * @param {wp.customize.selectiveRefresh.Partial} args.partial        The partial with which the container is associated.
 620           * @param {JQuery|Element}                        [args.container]    DOM element which contains the placement's contents.
 621           * @param {Node}                                  [args.startNode]    DOM node for the initial boundary of the placement.
 622           * @param {Node}                                  [args.endNode]      DOM node for the terminal boundary of the placement.
 623           * @param {Object}                                [args.context]      Context data included in the request in order to render the partial.
 624           * @param {string|Object|boolean}                 [args.addedContent] The content for the partial when refreshed.
 625           * @param {JQuery|DocumentFragment}               [args.removedNodes] DOM node(s) removed when the partial is refreshed.
 626           */
 627          initialize: function( args ) {
 628              var placement = this;
 629  
 630              args = _.extend( {}, args || {} );
 631              if ( ! args.partial || ! args.partial.extended( Partial ) ) {
 632                  throw new Error( 'Missing partial' );
 633              }
 634              args.context = args.context || {};
 635              if ( args.container ) {
 636                  args.container = $( args.container );
 637              }
 638  
 639              _.extend( placement, args );
 640          }
 641  
 642      });
 643  
 644      /**
 645       * Mapping of type names to Partial constructor subclasses.
 646       *
 647       * @since 4.5.0
 648       *
 649       * @type {Object.<string, wp.customize.selectiveRefresh.Partial>}
 650       */
 651      self.partialConstructor = {};
 652  
 653      self.partial = new api.Values({ defaultConstructor: Partial });
 654  
 655      /**
 656       * Get the POST vars for a Customizer preview request.
 657       *
 658       * @since 4.5.0
 659       * @see wp.customize.previewer.query()
 660       *
 661       * @return {Object} POST vars for a Customizer preview request.
 662       */
 663      self.getCustomizeQuery = function() {
 664          var dirtyCustomized = {};
 665          api.each( function( value, key ) {
 666              if ( value._dirty ) {
 667                  dirtyCustomized[ key ] = value();
 668              }
 669          } );
 670  
 671          return {
 672              wp_customize: 'on',
 673              nonce: api.settings.nonce.preview,
 674              customize_theme: api.settings.theme.stylesheet,
 675              customized: JSON.stringify( dirtyCustomized ),
 676              customize_changeset_uuid: api.settings.changeset.uuid
 677          };
 678      };
 679  
 680      /**
 681       * Currently-requested partials and their associated deferreds.
 682       *
 683       * @since 4.5.0
 684       * @type {Object.<string, { deferred: JQuery.Promise<*>, partial: wp.customize.selectiveRefresh.Partial }>}
 685       */
 686      self._pendingPartialRequests = {};
 687  
 688      /**
 689       * Timeout ID for the current request, or null if no request is current.
 690       *
 691       * @since 4.5.0
 692       * @type {number|null}
 693       * @private
 694       */
 695      self._debouncedTimeoutId = null;
 696  
 697      /**
 698       * Current jqXHR for the request to the partials.
 699       *
 700       * @since 4.5.0
 701       * @type {JQuery.jqXHR|null}
 702       * @private
 703       */
 704      self._currentRequest = null;
 705  
 706      /**
 707       * Request full page refresh.
 708       *
 709       * When selective refresh is embedded in the context of front-end editing, this request
 710       * must fail or else changes will be lost, unless transactions are implemented.
 711       *
 712       * @since 4.5.0
 713       */
 714      self.requestFullRefresh = function() {
 715          api.preview.send( 'refresh' );
 716      };
 717  
 718      /**
 719       * Request a re-rendering of a partial.
 720       *
 721       * @since 4.5.0
 722       *
 723       * @param {wp.customize.selectiveRefresh.Partial} partial The partial to re-render.
 724       * @return {JQuery.Promise<*>} Promise for the request to render the partial.
 725       */
 726      self.requestPartial = function( partial ) {
 727          var partialRequest;
 728  
 729          if ( self._debouncedTimeoutId ) {
 730              clearTimeout( self._debouncedTimeoutId );
 731              self._debouncedTimeoutId = null;
 732          }
 733          if ( self._currentRequest ) {
 734              self._currentRequest.abort();
 735              self._currentRequest = null;
 736          }
 737  
 738          partialRequest = self._pendingPartialRequests[ partial.id ];
 739          if ( ! partialRequest || 'pending' !== partialRequest.deferred.state() ) {
 740              partialRequest = {
 741                  deferred: $.Deferred(),
 742                  partial: partial
 743              };
 744              self._pendingPartialRequests[ partial.id ] = partialRequest;
 745          }
 746  
 747          // Prevent leaking partial into debounced timeout callback.
 748          partial = null;
 749  
 750          self._debouncedTimeoutId = setTimeout(
 751              function() {
 752                  var data, partialPlacementContexts, partialsPlacements, request;
 753  
 754                  self._debouncedTimeoutId = null;
 755                  data = self.getCustomizeQuery();
 756  
 757                  /*
 758                   * It is key that the containers be fetched exactly at the point of the request being
 759                   * made, because the containers need to be mapped to responses by array indices.
 760                   */
 761                  partialsPlacements = {};
 762  
 763                  partialPlacementContexts = {};
 764  
 765                  _.each( self._pendingPartialRequests, function( pending, partialId ) {
 766                      partialsPlacements[ partialId ] = pending.partial.placements();
 767                      if ( ! self.partial.has( partialId ) ) {
 768                          pending.deferred.rejectWith( pending.partial, [ new Error( 'partial_removed' ), partialsPlacements[ partialId ] ] );
 769                      } else {
 770                          /*
 771                           * Note that this may in fact be an empty array. In that case, it is the responsibility
 772                           * of the Partial subclass instance to know where to inject the response, or else to
 773                           * just issue a refresh (default behavior). The data being returned with each container
 774                           * is the context information that may be needed to render certain partials, such as
 775                           * the contained sidebar for rendering widgets or what the nav menu args are for a menu.
 776                           */
 777                          partialPlacementContexts[ partialId ] = _.map( partialsPlacements[ partialId ], function( placement ) {
 778                              return placement.context || {};
 779                          } );
 780                      }
 781                  } );
 782  
 783                  data.partials = JSON.stringify( partialPlacementContexts );
 784                  data[ self.data.renderQueryVar ] = '1';
 785  
 786                  request = self._currentRequest = wp.ajax.send( null, {
 787                      data: data,
 788                      url: api.settings.url.self
 789                  } );
 790  
 791                  request.done( function( data ) {
 792  
 793                      /**
 794                       * Announce the data returned from a request to render partials.
 795                       *
 796                       * The data is filtered on the server via customize_render_partials_response
 797                       * so plugins can inject data from the server to be utilized
 798                       * on the client via this event. Plugins may use this filter
 799                       * to communicate script and style dependencies that need to get
 800                       * injected into the page to support the rendered partials.
 801                       * This is similar to the 'saved' event.
 802                       */
 803                      self.trigger( 'render-partials-response', data );
 804  
 805                      // Relay errors (warnings) captured during rendering and relay to console.
 806                      if ( data.errors && 'undefined' !== typeof console && console.warn ) {
 807                          _.each( data.errors, function( error ) {
 808                              console.warn( error );
 809                          } );
 810                      }
 811  
 812                      /*
 813                       * Note that data is an array of items that correspond to the array of
 814                       * containers that were submitted in the request. So we zip up the
 815                       * array of containers with the array of contents for those containers,
 816                       * and send them into .
 817                       */
 818                      _.each( self._pendingPartialRequests, function( pending, partialId ) {
 819                          var placementsContents;
 820                          if ( ! _.isArray( data.contents[ partialId ] ) ) {
 821                              pending.deferred.rejectWith( pending.partial, [ new Error( 'unrecognized_partial' ), partialsPlacements[ partialId ] ] );
 822                          } else {
 823                              placementsContents = _.map( data.contents[ partialId ], function( content, i ) {
 824                                  var partialPlacement = partialsPlacements[ partialId ][ i ];
 825                                  if ( partialPlacement ) {
 826                                      partialPlacement.addedContent = content;
 827                                  } else {
 828                                      partialPlacement = new Placement( {
 829                                          partial: pending.partial,
 830                                          addedContent: content
 831                                      } );
 832                                  }
 833                                  return partialPlacement;
 834                              } );
 835                              pending.deferred.resolveWith( pending.partial, [ placementsContents ] );
 836                          }
 837                      } );
 838                      self._pendingPartialRequests = {};
 839                  } );
 840  
 841                  request.fail( function( data, statusText ) {
 842  
 843                      /*
 844                       * Ignore failures caused by partial.currentRequest.abort()
 845                       * The pending deferreds will remain in self._pendingPartialRequests
 846                       * for re-use with the next request.
 847                       */
 848                      if ( 'abort' === statusText ) {
 849                          return;
 850                      }
 851  
 852                      _.each( self._pendingPartialRequests, function( pending, partialId ) {
 853                          pending.deferred.rejectWith( pending.partial, [ data, partialsPlacements[ partialId ] ] );
 854                      } );
 855                      self._pendingPartialRequests = {};
 856                  } );
 857              },
 858              api.settings.timeouts.selectiveRefresh
 859          );
 860  
 861          return partialRequest.deferred.promise();
 862      };
 863  
 864      /**
 865       * Add partials for any nav menu container elements in the document.
 866       *
 867       * This method may be called multiple times. Containers that already have been
 868       * seen will be skipped.
 869       *
 870       * @since 4.5.0
 871       *
 872       * @param {JQuery|HTMLElement} [rootElement]                  Element to scan for partials. Defaults to the document element.
 873       * @param {Object}             [options]                      Options.
 874       * @param {boolean}            [options.triggerRendered=true] Whether to trigger the rendered event for the placements found.
 875       */
 876      self.addPartials = function( rootElement, options ) {
 877          var containerElements;
 878          if ( ! rootElement ) {
 879              rootElement = document.documentElement;
 880          }
 881          rootElement = $( rootElement );
 882          options = _.extend(
 883              {
 884                  triggerRendered: true
 885              },
 886              options || {}
 887          );
 888  
 889          containerElements = rootElement.find( '[data-customize-partial-id]' );
 890          if ( rootElement.is( '[data-customize-partial-id]' ) ) {
 891              containerElements = containerElements.add( rootElement );
 892          }
 893          containerElements.each( function() {
 894              var containerElement = $( this ), partial, placement, id, Constructor, partialOptions, containerContext;
 895              id = containerElement.data( 'customize-partial-id' );
 896              if ( ! id ) {
 897                  return;
 898              }
 899              containerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
 900  
 901              partial = self.partial( id );
 902              if ( ! partial ) {
 903                  partialOptions = containerElement.data( 'customize-partial-options' ) || {};
 904                  partialOptions.constructingContainerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
 905                  Constructor = self.partialConstructor[ containerElement.data( 'customize-partial-type' ) ] || self.Partial;
 906                  partial = new Constructor( id, partialOptions );
 907                  self.partial.add( partial );
 908              }
 909  
 910              /*
 911               * Only trigger renders on (nested) partials that have been not been
 912               * handled yet. An example where this would apply is a nav menu
 913               * embedded inside of a navigation menu widget. When the widget's title
 914               * is updated, the entire widget will re-render and then the event
 915               * will be triggered for the nested nav menu to do any initialization.
 916               */
 917              if ( options.triggerRendered && ! containerElement.data( 'customize-partial-content-rendered' ) ) {
 918  
 919                  placement = new Placement( {
 920                      partial: partial,
 921                      context: containerContext,
 922                      container: containerElement
 923                  } );
 924  
 925                  $( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit );
 926                  partial.createEditShortcutForPlacement( placement );
 927  
 928                  /**
 929                   * Announce when a partial's nested placement has been re-rendered.
 930                   */
 931                  self.trigger( 'partial-content-rendered', placement );
 932              }
 933              containerElement.data( 'customize-partial-content-rendered', true );
 934          } );
 935      };
 936  
 937      api.bind( 'preview-ready', function() {
 938          var handleSettingChange, watchSettingChange, unwatchSettingChange;
 939  
 940          _.extend( self.data, _customizePartialRefreshExports );
 941  
 942          // Create the partial JS models.
 943          _.each( self.data.partials, function( data, id ) {
 944              var Constructor, partial = self.partial( id );
 945              if ( ! partial ) {
 946                  Constructor = self.partialConstructor[ data.type ] || self.Partial;
 947                  partial = new Constructor(
 948                      id,
 949                      _.extend( { params: data }, data ) // Inclusion of params alias is for back-compat for custom partials that expect to augment this property.
 950                  );
 951                  self.partial.add( partial );
 952              } else {
 953                  _.extend( partial.params, data );
 954              }
 955          } );
 956  
 957          /**
 958           * Handle change to a setting.
 959           *
 960           * Note this is largely needed because adding a 'change' event handler to wp.customize
 961           * will only include the changed setting object as an argument, not including the
 962           * new value or the old value.
 963           *
 964           * @since 4.5.0
 965           * @this {wp.customize.Setting}
 966           *
 967           * @param {*|null} newValue New value, or null if the setting was just removed.
 968           * @param {*|null} oldValue Old value, or null if the setting was just added.
 969           */
 970          handleSettingChange = function( newValue, oldValue ) {
 971              var setting = this;
 972              self.partial.each( function( partial ) {
 973                  if ( partial.isRelatedSetting( setting, newValue, oldValue ) ) {
 974                      partial.refresh();
 975                  }
 976              } );
 977          };
 978  
 979          /**
 980           * Trigger the initial change for the added setting, and watch for changes.
 981           *
 982           * @since 4.5.0
 983           * @this {wp.customize.Values}
 984           *
 985           * @param {wp.customize.Setting} setting The setting that was added.
 986           */
 987          watchSettingChange = function( setting ) {
 988              handleSettingChange.call( setting, setting(), null );
 989              setting.bind( handleSettingChange );
 990          };
 991  
 992          /**
 993           * Trigger the final change for the removed setting, and unwatch for changes.
 994           *
 995           * @since 4.5.0
 996           * @this {wp.customize.Values}
 997           *
 998           * @param {wp.customize.Setting} setting The setting that was removed.
 999           */
1000          unwatchSettingChange = function( setting ) {
1001              handleSettingChange.call( setting, null, setting() );
1002              setting.unbind( handleSettingChange );
1003          };
1004  
1005          api.bind( 'add', watchSettingChange );
1006          api.bind( 'remove', unwatchSettingChange );
1007          api.each( function( setting ) {
1008              setting.bind( handleSettingChange );
1009          } );
1010  
1011          // Add (dynamic) initial partials that are declared via data-* attributes.
1012          self.addPartials( document.documentElement, {
1013              triggerRendered: false
1014          } );
1015  
1016          // Add new dynamic partials when the document changes.
1017          if ( 'undefined' !== typeof MutationObserver ) {
1018              self.mutationObserver = new MutationObserver( function( mutations ) {
1019                  _.each( mutations, function( mutation ) {
1020                      self.addPartials( $( mutation.target ) );
1021                  } );
1022              } );
1023              self.mutationObserver.observe( document.documentElement, {
1024                  childList: true,
1025                  subtree: true
1026              } );
1027          }
1028  
1029          /**
1030           * Handle rendering of partials.
1031           *
1032           * @param {wp.customize.selectiveRefresh.Placement} placement The placement whose content was rendered.
1033           */
1034          api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
1035              if ( placement.container ) {
1036                  self.addPartials( placement.container );
1037              }
1038          } );
1039  
1040          /**
1041           * Handle setting validities in partial refresh response.
1042           *
1043           * @param {Object} data                    Response data.
1044           * @param {Object} data.setting_validities Setting validities.
1045           */
1046          api.selectiveRefresh.bind( 'render-partials-response', function handleSettingValiditiesResponse( data ) {
1047              if ( data.setting_validities ) {
1048                  api.preview.send( 'selective-refresh-setting-validities', data.setting_validities );
1049              }
1050          } );
1051  
1052          api.preview.bind( 'edit-shortcut-visibility', function( visibility ) {
1053              api.selectiveRefresh.editShortcutVisibility.set( visibility );
1054          } );
1055          api.selectiveRefresh.editShortcutVisibility.bind( function( visibility ) {
1056              var body = $( document.body ), shouldAnimateHide;
1057  
1058              shouldAnimateHide = ( 'hidden' === visibility && body.hasClass( 'customize-partial-edit-shortcuts-shown' ) && ! body.hasClass( 'customize-partial-edit-shortcuts-hidden' ) );
1059              body.toggleClass( 'customize-partial-edit-shortcuts-hidden', shouldAnimateHide );
1060              body.toggleClass( 'customize-partial-edit-shortcuts-shown', 'visible' === visibility );
1061          } );
1062  
1063          api.preview.bind( 'active', function() {
1064  
1065              // Make all partials ready.
1066              self.partial.each( function( partial ) {
1067                  partial.deferred.ready.resolve();
1068              } );
1069  
1070              // Make all partials added henceforth as ready upon add.
1071              self.partial.bind( 'add', function( partial ) {
1072                  partial.deferred.ready.resolve();
1073              } );
1074          } );
1075  
1076      } );
1077  
1078      return self;
1079  }( jQuery, wp.customize ) );


Generated : Tue Sep 15 08:20:32 2026 Cross-referenced by PHPXref