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


Generated : Thu Nov 21 08:20:01 2024 Cross-referenced by PHPXref