[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  /**
   2   * @output wp-includes/js/customize-preview.js
   3   */
   4  
   5  /**
   6   * Script run inside a Customizer preview frame.
   7   *
   8   * @param {Object}       wp The WordPress global object.
   9   * @param {JQueryStatic} $  The jQuery object.
  10   */
  11  (function( wp, $ ){
  12      var api = wp.customize,
  13          debounce,
  14          currentHistoryState = {};
  15  
  16      /*
  17       * Capture the state that is passed into history.replaceState() and history.pushState()
  18       * and also which is returned in the popstate event so that when the changeset_uuid
  19       * gets updated when transitioning to a new changeset there the current state will
  20       * be supplied in the call to history.replaceState().
  21       */
  22      ( function( history ) {
  23          var injectUrlWithState;
  24  
  25          if ( ! history.replaceState ) {
  26              return;
  27          }
  28  
  29          /**
  30           * Amend the supplied URL with the customized state.
  31           *
  32           * @since 4.7.0
  33           * @access private
  34           *
  35           * @param {string} url URL.
  36           * @return {string} URL with customized state.
  37           */
  38          injectUrlWithState = function( url ) {
  39              var urlParser, oldQueryParams, newQueryParams;
  40              urlParser = document.createElement( 'a' );
  41              urlParser.href = url;
  42              oldQueryParams = api.utils.parseQueryString( location.search.substr( 1 ) );
  43              newQueryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  44  
  45              newQueryParams.customize_changeset_uuid = oldQueryParams.customize_changeset_uuid;
  46              if ( oldQueryParams.customize_autosaved ) {
  47                  newQueryParams.customize_autosaved = 'on';
  48              }
  49              if ( oldQueryParams.customize_theme ) {
  50                  newQueryParams.customize_theme = oldQueryParams.customize_theme;
  51              }
  52              if ( oldQueryParams.customize_messenger_channel ) {
  53                  newQueryParams.customize_messenger_channel = oldQueryParams.customize_messenger_channel;
  54              }
  55              urlParser.search = $.param( newQueryParams );
  56              return urlParser.href;
  57          };
  58  
  59          history.replaceState = ( function( nativeReplaceState ) {
  60              return function historyReplaceState( data, title, url ) {
  61                  currentHistoryState = data;
  62                  return nativeReplaceState.call( history, data, title, 'string' === typeof url && url.length > 0 ? injectUrlWithState( url ) : url );
  63              };
  64          } )( history.replaceState );
  65  
  66          history.pushState = ( function( nativePushState ) {
  67              return function historyPushState( data, title, url ) {
  68                  currentHistoryState = data;
  69                  return nativePushState.call( history, data, title, 'string' === typeof url && url.length > 0 ? injectUrlWithState( url ) : url );
  70              };
  71          } )( history.pushState );
  72  
  73          window.addEventListener( 'popstate', function( event ) {
  74              currentHistoryState = event.state;
  75          } );
  76  
  77      }( history ) );
  78  
  79      /**
  80       * Returns a debounced version of the function.
  81       *
  82       * @param {Function} fn        Function to debounce.
  83       * @param {number}   delay     Delay in milliseconds.
  84       * @param {Object}   [context] Context to invoke the function in. Defaults to the calling context.
  85       * @todo Require Underscore.js for this file and retire this.
  86       *
  87       * @return {Function} Debounced function.
  88       */
  89      debounce = function( fn, delay, context ) {
  90          var timeout;
  91          return function( ...args ) {
  92              context = context || this;
  93  
  94              clearTimeout( timeout );
  95              timeout = setTimeout( function() {
  96                  timeout = null;
  97                  fn.apply( context, args );
  98              }, delay );
  99          };
 100      };
 101  
 102      /**
 103       * @memberOf wp.customize
 104       * @alias wp.customize.Preview
 105       *
 106       * @class
 107       * @augments wp.customize.Messenger
 108       * @augments wp.customize.Class
 109       * @mixes wp.customize.Events
 110       */
 111      api.Preview = api.Messenger.extend(/** @lends wp.customize.Preview.prototype */{
 112          /**
 113           * Initializes the previewer.
 114           *
 115           * @param {Object} params  Parameters to configure the messenger.
 116           * @param {Object} options Extend any instance parameter or method with this object.
 117           */
 118          initialize: function( params, options ) {
 119              var preview = this, urlParser = document.createElement( 'a' );
 120  
 121              api.Messenger.prototype.initialize.call( preview, params, options );
 122  
 123              urlParser.href = preview.origin();
 124              preview.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
 125  
 126              preview.body = $( document.body );
 127              preview.window = $( window );
 128  
 129              if ( api.settings.channel ) {
 130  
 131                  // If in an iframe, then intercept the link clicks and form submissions.
 132                  preview.body.on( 'click.preview', 'a', function( event ) {
 133                      preview.handleLinkClick( event );
 134                  } );
 135                  preview.body.on( 'submit.preview', 'form', function( event ) {
 136                      preview.handleFormSubmit( event );
 137                  } );
 138  
 139                  preview.window.on( 'scroll.preview', debounce( function() {
 140                      preview.send( 'scroll', preview.window.scrollTop() );
 141                  }, 200 ) );
 142  
 143                  preview.bind( 'scroll', function( distance ) {
 144                      preview.window.scrollTop( distance );
 145                  });
 146              }
 147          },
 148  
 149          /**
 150           * Handle link clicks in preview.
 151           *
 152           * @since 4.7.0
 153           * @access public
 154           *
 155           * @param {JQuery.Event} event Event.
 156           * @return {void}
 157           */
 158          handleLinkClick: function( event ) {
 159              var preview = this, link, isInternalJumpLink;
 160              link = $( event.target ).closest( 'a' );
 161  
 162              // No-op if the anchor is not a link.
 163              if ( _.isUndefined( link.attr( 'href' ) ) ) {
 164                  return;
 165              }
 166  
 167              // Allow internal jump links and JS links to behave normally without preventing default.
 168              isInternalJumpLink = ( '#' === link.attr( 'href' ).substr( 0, 1 ) );
 169              if ( isInternalJumpLink || ! /^https?:$/.test( link.prop( 'protocol' ) ) ) {
 170                  return;
 171              }
 172  
 173              // If the link is not previewable, prevent the browser from navigating to it.
 174              if ( ! api.isLinkPreviewable( link[0] ) ) {
 175                  wp.a11y.speak( api.settings.l10n.linkUnpreviewable );
 176                  event.preventDefault();
 177                  return;
 178              }
 179  
 180              // Prevent initiating navigating from click and instead rely on sending url message to pane.
 181              event.preventDefault();
 182  
 183              /*
 184               * Note the shift key is checked so shift+click on widgets or
 185               * nav menu items can just result on focusing on the corresponding
 186               * control instead of also navigating to the URL linked to.
 187               */
 188              if ( event.shiftKey ) {
 189                  return;
 190              }
 191  
 192              // Note: It's not relevant to send scroll because sending url message will have the same effect.
 193              preview.send( 'url', link.prop( 'href' ) );
 194          },
 195  
 196          /**
 197           * Handle form submit.
 198           *
 199           * @since 4.7.0
 200           * @access public
 201           *
 202           * @param {JQuery.Event} event Event.
 203           * @return {void}
 204           */
 205          handleFormSubmit: function( event ) {
 206              var preview = this, urlParser, form;
 207              urlParser = document.createElement( 'a' );
 208              form = $( event.target );
 209              urlParser.href = form.prop( 'action' );
 210  
 211              // If the link is not previewable, prevent the browser from navigating to it.
 212              if ( 'GET' !== form.prop( 'method' ).toUpperCase() || ! api.isLinkPreviewable( urlParser ) ) {
 213                  wp.a11y.speak( api.settings.l10n.formUnpreviewable );
 214                  event.preventDefault();
 215                  return;
 216              }
 217  
 218              /*
 219               * If the default wasn't prevented already (in which case the form
 220               * submission is already being handled by JS), and if it has a GET
 221               * request method, then take the serialized form data and add it as
 222               * a query string to the action URL and send this in a url message
 223               * to the customizer pane so that it will be loaded. If the form's
 224               * action points to a non-previewable URL, the customizer pane's
 225               * previewUrl setter will reject it so that the form submission is
 226               * a no-op, which is the same behavior as when clicking a link to an
 227               * external site in the preview.
 228               */
 229              if ( ! event.isDefaultPrevented() ) {
 230                  if ( urlParser.search.length > 1 ) {
 231                      urlParser.search += '&';
 232                  }
 233                  urlParser.search += form.serialize();
 234                  preview.send( 'url', urlParser.href );
 235              }
 236  
 237              // Prevent default since navigation should be done via sending url message or via JS submit handler.
 238              event.preventDefault();
 239          }
 240      });
 241  
 242      /**
 243       * Inject the changeset UUID into links in the document.
 244       *
 245       * @since 4.7.0
 246       * @access protected
 247       *
 248       * @return {void}
 249       */
 250      api.addLinkPreviewing = function addLinkPreviewing() {
 251          var linkSelectors = 'a[href], area[href]';
 252  
 253          // Inject links into initial document.
 254          $( document.body ).find( linkSelectors ).each( function() {
 255              api.prepareLinkPreview( this );
 256          } );
 257  
 258          // Inject links for new elements added to the page.
 259          if ( 'undefined' !== typeof MutationObserver ) {
 260              api.mutationObserver = new MutationObserver( function( mutations ) {
 261                  _.each( mutations, function( mutation ) {
 262                      $( mutation.target ).find( linkSelectors ).each( function() {
 263                          api.prepareLinkPreview( this );
 264                      } );
 265                  } );
 266              } );
 267              api.mutationObserver.observe( document.documentElement, {
 268                  childList: true,
 269                  subtree: true
 270              } );
 271          } else {
 272  
 273              // If mutation observers aren't available, fallback to just-in-time injection.
 274              $( document.documentElement ).on( 'click focus mouseover', linkSelectors, function() {
 275                  api.prepareLinkPreview( this );
 276              } );
 277          }
 278      };
 279  
 280      /**
 281       * Should the supplied link is previewable.
 282       *
 283       * @since 4.7.0
 284       * @access public
 285       *
 286       * @param {HTMLAnchorElement|HTMLAreaElement} element                        Link element.
 287       * @param {string}                            element.search                 Query string.
 288       * @param {string}                            element.pathname               Path.
 289       * @param {string}                            element.host                   Host.
 290       * @param {Object}                            [options]                      Options.
 291       * @param {Object}                            [options.allowAdminAjax=false] Allow admin-ajax.php requests.
 292       * @return {boolean} Is appropriate for changeset link.
 293       */
 294      api.isLinkPreviewable = function isLinkPreviewable( element, options ) {
 295          var matchesAllowedUrl, parsedAllowedUrl, args, elementHost;
 296  
 297          args = _.extend( {}, { allowAdminAjax: false }, options || {} );
 298  
 299          if ( 'javascript:' === element.protocol ) { // jshint ignore:line
 300              return true;
 301          }
 302  
 303          // Only web URLs can be previewed.
 304          if ( 'https:' !== element.protocol && 'http:' !== element.protocol ) {
 305              return false;
 306          }
 307  
 308          elementHost = element.host.replace( /:(80|443)$/, '' );
 309          parsedAllowedUrl = document.createElement( 'a' );
 310          matchesAllowedUrl = ! _.isUndefined( _.find( api.settings.url.allowed, function( allowedUrl ) {
 311              parsedAllowedUrl.href = allowedUrl;
 312              return parsedAllowedUrl.protocol === element.protocol && parsedAllowedUrl.host.replace( /:(80|443)$/, '' ) === elementHost && 0 === element.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) );
 313          } ) );
 314          if ( ! matchesAllowedUrl ) {
 315              return false;
 316          }
 317  
 318          // Skip wp login and signup pages.
 319          if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) {
 320              return false;
 321          }
 322  
 323          // Allow links to admin ajax as faux frontend URLs.
 324          if ( /\/wp-admin\/admin-ajax\.php$/.test( element.pathname ) ) {
 325              return args.allowAdminAjax;
 326          }
 327  
 328          // Disallow links to admin, includes, and content.
 329          if ( /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) {
 330              return false;
 331          }
 332  
 333          return true;
 334      };
 335  
 336      /**
 337       * Inject the customize_changeset_uuid query param into links on the frontend.
 338       *
 339       * @since 4.7.0
 340       * @access protected
 341       *
 342       * @param {HTMLAnchorElement|HTMLAreaElement} element          Link element.
 343       * @param {string}                            element.search   Query string.
 344       * @param {string}                            element.host     Host.
 345       * @param {string}                            element.protocol Protocol.
 346       * @return {void}
 347       */
 348      api.prepareLinkPreview = function prepareLinkPreview( element ) {
 349          var queryParams, $element = $( element );
 350  
 351          // Skip elements with no href attribute. Check first to avoid more expensive checks down the road.
 352          if ( ! element.hasAttribute( 'href' ) ) {
 353              return;
 354          }
 355  
 356          // Skip links in admin bar.
 357          if ( $element.closest( '#wpadminbar' ).length ) {
 358              return;
 359          }
 360  
 361          // Ignore links with href="#", href="#id", or non-HTTP protocols (e.g. javascript: and mailto:).
 362          if ( '#' === $element.attr( 'href' ).substr( 0, 1 ) || ! /^https?:$/.test( element.protocol ) ) {
 363              return;
 364          }
 365  
 366          // Make sure links in preview use HTTPS if parent frame uses HTTPS.
 367          if ( api.settings.channel && 'https' === api.preview.scheme.get() && 'http:' === element.protocol && -1 !== api.settings.url.allowedHosts.indexOf( element.host ) ) {
 368              element.protocol = 'https:';
 369          }
 370  
 371          // Ignore links with class wp-playlist-caption.
 372          if ( $element.hasClass( 'wp-playlist-caption' ) ) {
 373              return;
 374          }
 375  
 376          if ( ! api.isLinkPreviewable( element ) ) {
 377  
 378              // Style link as unpreviewable only if previewing in iframe; if previewing on frontend, links will be allowed to work normally.
 379              if ( api.settings.channel ) {
 380                  $element.addClass( 'customize-unpreviewable' );
 381              }
 382              return;
 383          }
 384          $element.removeClass( 'customize-unpreviewable' );
 385  
 386          queryParams = api.utils.parseQueryString( element.search.substring( 1 ) );
 387          queryParams.customize_changeset_uuid = api.settings.changeset.uuid;
 388          if ( api.settings.changeset.autosaved ) {
 389              queryParams.customize_autosaved = 'on';
 390          }
 391          if ( ! api.settings.theme.active ) {
 392              queryParams.customize_theme = api.settings.theme.stylesheet;
 393          }
 394          if ( api.settings.channel ) {
 395              queryParams.customize_messenger_channel = api.settings.channel;
 396          }
 397          element.search = $.param( queryParams );
 398      };
 399  
 400      /**
 401       * Inject the changeset UUID into Ajax requests.
 402       *
 403       * @since 4.7.0
 404       * @access protected
 405       *
 406       * @return {void}
 407       */
 408      api.addRequestPreviewing = function addRequestPreviewing() {
 409  
 410          /**
 411           * Rewrite Ajax requests to inject customizer state.
 412           *
 413           * @param {Object}         options         Options.
 414           * @param {string}         options.type    Type.
 415           * @param {string}         options.url     URL.
 416           * @param {Object}         originalOptions Original options.
 417           * @param {XMLHttpRequest} xhr             XHR.
 418           * @return {void}
 419           */
 420          var prefilterAjax = function( options, originalOptions, xhr ) {
 421              var urlParser, queryParams, requestMethod, dirtyValues = {};
 422              urlParser = document.createElement( 'a' );
 423              urlParser.href = options.url;
 424  
 425              // Abort if the request is not for this site.
 426              if ( ! api.isLinkPreviewable( urlParser, { allowAdminAjax: true } ) ) {
 427                  return;
 428              }
 429              queryParams = api.utils.parseQueryString( urlParser.search.substring( 1 ) );
 430  
 431              // Note that _dirty flag will be cleared with changeset updates.
 432              api.each( function( setting ) {
 433                  if ( setting._dirty ) {
 434                      dirtyValues[ setting.id ] = setting.get();
 435                  }
 436              } );
 437  
 438              if ( ! _.isEmpty( dirtyValues ) ) {
 439                  requestMethod = options.type.toUpperCase();
 440  
 441                  // Override underlying request method to ensure unsaved changes to changeset can be included (force Backbone.emulateHTTP).
 442                  if ( 'POST' !== requestMethod ) {
 443                      xhr.setRequestHeader( 'X-HTTP-Method-Override', requestMethod );
 444                      queryParams._method = requestMethod;
 445                      options.type = 'POST';
 446                  }
 447  
 448                  // Amend the post data with the customized values.
 449                  if ( options.data ) {
 450                      options.data += '&';
 451                  } else {
 452                      options.data = '';
 453                  }
 454                  options.data += $.param( {
 455                      customized: JSON.stringify( dirtyValues )
 456                  } );
 457              }
 458  
 459              // Include customized state query params in URL.
 460              queryParams.customize_changeset_uuid = api.settings.changeset.uuid;
 461              if ( api.settings.changeset.autosaved ) {
 462                  queryParams.customize_autosaved = 'on';
 463              }
 464              if ( ! api.settings.theme.active ) {
 465                  queryParams.customize_theme = api.settings.theme.stylesheet;
 466              }
 467  
 468              // Ensure preview nonce is included with every customized request, to allow post data to be read.
 469              queryParams.customize_preview_nonce = api.settings.nonce.preview;
 470  
 471              urlParser.search = $.param( queryParams );
 472              options.url = urlParser.href;
 473          };
 474  
 475          $.ajaxPrefilter( prefilterAjax );
 476      };
 477  
 478      /**
 479       * Inject changeset UUID into forms, allowing preview to persist through submissions.
 480       *
 481       * @since 4.7.0
 482       * @access protected
 483       *
 484       * @return {void}
 485       */
 486      api.addFormPreviewing = function addFormPreviewing() {
 487  
 488          // Inject inputs for forms in initial document.
 489          $( document.body ).find( 'form' ).each( function() {
 490              api.prepareFormPreview( this );
 491          } );
 492  
 493          // Inject inputs for new forms added to the page.
 494          if ( 'undefined' !== typeof MutationObserver ) {
 495              api.mutationObserver = new MutationObserver( function( mutations ) {
 496                  _.each( mutations, function( mutation ) {
 497                      $( mutation.target ).find( 'form' ).each( function() {
 498                          api.prepareFormPreview( this );
 499                      } );
 500                  } );
 501              } );
 502              api.mutationObserver.observe( document.documentElement, {
 503                  childList: true,
 504                  subtree: true
 505              } );
 506          }
 507      };
 508  
 509      /**
 510       * Inject changeset into form inputs.
 511       *
 512       * @since 4.7.0
 513       * @access protected
 514       *
 515       * @param {HTMLFormElement} form Form.
 516       * @return {void}
 517       */
 518      api.prepareFormPreview = function prepareFormPreview( form ) {
 519          var urlParser, stateParams = {};
 520  
 521          if ( ! form.action ) {
 522              form.action = location.href;
 523          }
 524  
 525          urlParser = document.createElement( 'a' );
 526          urlParser.href = form.action;
 527  
 528          // Make sure forms in preview use HTTPS if parent frame uses HTTPS.
 529          if ( api.settings.channel && 'https' === api.preview.scheme.get() && 'http:' === urlParser.protocol && -1 !== api.settings.url.allowedHosts.indexOf( urlParser.host ) ) {
 530              urlParser.protocol = 'https:';
 531              form.action = urlParser.href;
 532          }
 533  
 534          if ( 'GET' !== form.method.toUpperCase() || ! api.isLinkPreviewable( urlParser ) ) {
 535  
 536              // Style form as unpreviewable only if previewing in iframe; if previewing on frontend, all forms will be allowed to work normally.
 537              if ( api.settings.channel ) {
 538                  $( form ).addClass( 'customize-unpreviewable' );
 539              }
 540              return;
 541          }
 542          $( form ).removeClass( 'customize-unpreviewable' );
 543  
 544          stateParams.customize_changeset_uuid = api.settings.changeset.uuid;
 545          if ( api.settings.changeset.autosaved ) {
 546              stateParams.customize_autosaved = 'on';
 547          }
 548          if ( ! api.settings.theme.active ) {
 549              stateParams.customize_theme = api.settings.theme.stylesheet;
 550          }
 551          if ( api.settings.channel ) {
 552              stateParams.customize_messenger_channel = api.settings.channel;
 553          }
 554  
 555          _.each( stateParams, function( value, name ) {
 556              var input = $( form ).find( 'input[name="' + name + '"]' );
 557              if ( input.length ) {
 558                  input.val( value );
 559              } else {
 560                  $( form ).prepend( $( '<input>', {
 561                      type: 'hidden',
 562                      name: name,
 563                      value: value
 564                  } ) );
 565              }
 566          } );
 567  
 568          // Prevent links from breaking out of preview iframe.
 569          if ( api.settings.channel ) {
 570              form.target = '_self';
 571          }
 572      };
 573  
 574      /**
 575       * Watch current URL and send keep-alive (heartbeat) messages to the parent.
 576       *
 577       * Keep the customizer pane notified that the preview is still alive
 578       * and that the user hasn't navigated to a non-customized URL.
 579       *
 580       * @since 4.7.0
 581       * @access protected
 582       */
 583      api.keepAliveCurrentUrl = ( function() {
 584          var previousPathName = location.pathname,
 585              previousQueryString = location.search.substr( 1 ),
 586              previousQueryParams = null,
 587              stateQueryParams = [ 'customize_theme', 'customize_changeset_uuid', 'customize_messenger_channel', 'customize_autosaved' ];
 588  
 589          return function keepAliveCurrentUrl() {
 590              var urlParser, currentQueryParams;
 591  
 592              // Short-circuit with keep-alive if previous URL is identical (as is normal case).
 593              if ( previousQueryString === location.search.substr( 1 ) && previousPathName === location.pathname ) {
 594                  api.preview.send( 'keep-alive' );
 595                  return;
 596              }
 597  
 598              urlParser = document.createElement( 'a' );
 599              if ( null === previousQueryParams ) {
 600                  urlParser.search = previousQueryString;
 601                  previousQueryParams = api.utils.parseQueryString( previousQueryString );
 602                  _.each( stateQueryParams, function( name ) {
 603                      delete previousQueryParams[ name ];
 604                  } );
 605              }
 606  
 607              // Determine if current URL minus customized state params and URL hash.
 608              urlParser.href = location.href;
 609              currentQueryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
 610              _.each( stateQueryParams, function( name ) {
 611                  delete currentQueryParams[ name ];
 612              } );
 613  
 614              if ( previousPathName !== location.pathname || ! _.isEqual( previousQueryParams, currentQueryParams ) ) {
 615                  urlParser.search = $.param( currentQueryParams );
 616                  urlParser.hash = '';
 617                  api.settings.url.self = urlParser.href;
 618                  api.preview.send( 'ready', {
 619                      currentUrl: api.settings.url.self,
 620                      activePanels: api.settings.activePanels,
 621                      activeSections: api.settings.activeSections,
 622                      activeControls: api.settings.activeControls,
 623                      settingValidities: api.settings.settingValidities
 624                  } );
 625              } else {
 626                  api.preview.send( 'keep-alive' );
 627              }
 628              previousQueryParams = currentQueryParams;
 629              previousQueryString = location.search.substr( 1 );
 630              previousPathName = location.pathname;
 631          };
 632      } )();
 633  
 634      api.settingPreviewHandlers = {
 635  
 636          /**
 637           * Preview changes to custom logo.
 638           *
 639           * @param {number} attachmentId Attachment ID for custom logo.
 640           * @return {void}
 641           */
 642          custom_logo: function( attachmentId ) {
 643              $( 'body' ).toggleClass( 'wp-custom-logo', !! attachmentId );
 644          },
 645  
 646          /**
 647           * Preview changes to custom css.
 648           *
 649           * @param {string} value Custom CSS.
 650           * @return {void}
 651           */
 652          custom_css: function( value ) {
 653              var style;
 654              if ( api.settings.theme.isBlockTheme ) {
 655                  style = $( 'style#global-styles-inline-css' );
 656  
 657                  // Forbid milestone comments from appearing in Custom CSS which would break live preview.
 658                  value = value.replace( /\/\*(BEGIN|END)_CUSTOMIZER_CUSTOM_CSS\*\//g, '' );
 659  
 660                  var textContent = style.text().replace(
 661                      /(\/\*BEGIN_CUSTOMIZER_CUSTOM_CSS\*\/)((?:.|\s)*?)(\/\*END_CUSTOMIZER_CUSTOM_CSS\*\/)/,
 662                      function ( match, beforeComment, oldValue, afterComment ) {
 663                          return beforeComment + '\n' + value + '\n' + afterComment;
 664                      }
 665                  );
 666                  style.text( textContent );
 667              } else {
 668                  style = $( 'style#wp-custom-css' );
 669                  style.text( value );
 670              }
 671          },
 672  
 673          /**
 674           * Preview changes to any of the background settings.
 675           *
 676           * @return {void}
 677           */
 678          background: function() {
 679              var css = '', settings = {};
 680  
 681              _.each( ['color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment'], function( prop ) {
 682                  settings[ prop ] = api( 'background_' + prop );
 683              } );
 684  
 685              /*
 686               * The body will support custom backgrounds if either the color or image are set.
 687               *
 688               * See get_body_class() in /wp-includes/post-template.php
 689               */
 690              $( document.body ).toggleClass( 'custom-background', !! ( settings.color() || settings.image() ) );
 691  
 692              if ( settings.color() ) {
 693                  css += 'background-color: ' + settings.color() + ';';
 694              }
 695  
 696              if ( settings.image() ) {
 697                  css += 'background-image: url("' + settings.image() + '");';
 698                  css += 'background-size: ' + settings.size() + ';';
 699                  css += 'background-position: ' + settings.position_x() + ' ' + settings.position_y() + ';';
 700                  css += 'background-repeat: ' + settings.repeat() + ';';
 701                  css += 'background-attachment: ' + settings.attachment() + ';';
 702              }
 703  
 704              $( '#custom-background-css' ).text( 'body.custom-background { ' + css + ' }' );
 705          }
 706      };
 707  
 708      $( function() {
 709          var bg, setValue, handleUpdatedChangesetUuid;
 710  
 711          api.settings = window._wpCustomizeSettings;
 712          if ( ! api.settings ) {
 713              return;
 714          }
 715  
 716          api.preview = new api.Preview({
 717              url: window.location.href,
 718              channel: api.settings.channel
 719          });
 720  
 721          api.addLinkPreviewing();
 722          api.addRequestPreviewing();
 723          api.addFormPreviewing();
 724  
 725          /**
 726           * Create/update a setting value.
 727           *
 728           * @param {string}  id            Setting ID.
 729           * @param {*}       value         Setting value.
 730           * @param {boolean} [createDirty] Whether to create a setting as dirty. Defaults to false.
 731           */
 732          setValue = function( id, value, createDirty ) {
 733              var setting = api( id );
 734              if ( setting ) {
 735                  setting.set( value );
 736              } else {
 737                  createDirty = createDirty || false;
 738                  setting = api.create( id, value, {
 739                      id: id
 740                  } );
 741  
 742                  // Mark dynamically-created settings as dirty so they will get posted.
 743                  if ( createDirty ) {
 744                      setting._dirty = true;
 745                  }
 746              }
 747          };
 748  
 749          api.preview.bind( 'settings', function( values ) {
 750              $.each( values, setValue );
 751          });
 752  
 753          api.preview.trigger( 'settings', api.settings.values );
 754  
 755          $.each( api.settings._dirty, function( i, id ) {
 756              var setting = api( id );
 757              if ( setting ) {
 758                  setting._dirty = true;
 759              }
 760          } );
 761  
 762          api.preview.bind( 'setting', function( args ) {
 763              var createDirty = true;
 764              setValue.apply( null, args.concat( createDirty ) );
 765          });
 766  
 767          api.preview.bind( 'sync', function( events ) {
 768  
 769              /*
 770               * Delete any settings that already exist locally which haven't been
 771               * modified in the controls while the preview was loading. This prevents
 772               * situations where the JS value being synced from the pane may differ
 773               * from the PHP-sanitized JS value in the preview which causes the
 774               * non-sanitized JS value to clobber the PHP-sanitized value. This
 775               * is particularly important for selective refresh partials that
 776               * have a fallback refresh behavior since infinite refreshing would
 777               * result.
 778               */
 779              if ( events.settings && events['settings-modified-while-loading'] ) {
 780                  _.each( _.keys( events.settings ), function( syncedSettingId ) {
 781                      if ( api.has( syncedSettingId ) && ! events['settings-modified-while-loading'][ syncedSettingId ] ) {
 782                          delete events.settings[ syncedSettingId ];
 783                      }
 784                  } );
 785              }
 786  
 787              $.each( events, function( event, args ) {
 788                  api.preview.trigger( event, args );
 789              });
 790              api.preview.send( 'synced' );
 791          });
 792  
 793          api.preview.bind( 'active', function() {
 794              api.preview.send( 'nonce', api.settings.nonce );
 795  
 796              api.preview.send( 'documentTitle', document.title );
 797  
 798              // Send scroll in case of loading via non-refresh.
 799              api.preview.send( 'scroll', $( window ).scrollTop() );
 800          });
 801  
 802          /**
 803           * Handle update to changeset UUID.
 804           *
 805           * @param {string} uuid UUID.
 806           * @return {void}
 807           */
 808          handleUpdatedChangesetUuid = function( uuid ) {
 809              api.settings.changeset.uuid = uuid;
 810  
 811              // Update UUIDs in links and forms.
 812              $( document.body ).find( 'a[href], area[href]' ).each( function() {
 813                  api.prepareLinkPreview( this );
 814              } );
 815              $( document.body ).find( 'form' ).each( function() {
 816                  api.prepareFormPreview( this );
 817              } );
 818  
 819              /*
 820               * Replace the UUID in the URL. Note that the wrapped history.replaceState()
 821               * will handle injecting the current api.settings.changeset.uuid into the URL,
 822               * so this is merely to trigger that logic.
 823               */
 824              if ( history.replaceState ) {
 825                  history.replaceState( currentHistoryState, '', location.href );
 826              }
 827          };
 828  
 829          api.preview.bind( 'changeset-uuid', handleUpdatedChangesetUuid );
 830  
 831          api.preview.bind( 'saved', function( response ) {
 832              if ( response.next_changeset_uuid ) {
 833                  handleUpdatedChangesetUuid( response.next_changeset_uuid );
 834              }
 835              api.trigger( 'saved', response );
 836          } );
 837  
 838          // Update the URLs to reflect the fact we've started autosaving.
 839          api.preview.bind( 'autosaving', function() {
 840              if ( api.settings.changeset.autosaved ) {
 841                  return;
 842              }
 843  
 844              api.settings.changeset.autosaved = true; // Start deferring to any autosave once changeset is updated.
 845  
 846              $( document.body ).find( 'a[href], area[href]' ).each( function() {
 847                  api.prepareLinkPreview( this );
 848              } );
 849              $( document.body ).find( 'form' ).each( function() {
 850                  api.prepareFormPreview( this );
 851              } );
 852              if ( history.replaceState ) {
 853                  history.replaceState( currentHistoryState, '', location.href );
 854              }
 855          } );
 856  
 857          /*
 858           * Clear dirty flag for settings when saved to changeset so that they
 859           * won't be needlessly included in selective refresh or ajax requests.
 860           */
 861          api.preview.bind( 'changeset-saved', function( data ) {
 862              _.each( data.saved_changeset_values, function( value, settingId ) {
 863                  var setting = api( settingId );
 864                  if ( setting && _.isEqual( setting.get(), value ) ) {
 865                      setting._dirty = false;
 866                  }
 867              } );
 868          } );
 869  
 870          api.preview.bind( 'nonce-refresh', function( nonce ) {
 871              $.extend( api.settings.nonce, nonce );
 872          } );
 873  
 874          /*
 875           * Send a message to the parent customize frame with a list of which
 876           * containers and controls are active.
 877           */
 878          api.preview.send( 'ready', {
 879              currentUrl: api.settings.url.self,
 880              activePanels: api.settings.activePanels,
 881              activeSections: api.settings.activeSections,
 882              activeControls: api.settings.activeControls,
 883              settingValidities: api.settings.settingValidities
 884          } );
 885  
 886          // Send ready when URL changes via JS.
 887          setInterval( api.keepAliveCurrentUrl, api.settings.timeouts.keepAliveSend );
 888  
 889          // Display a loading indicator when preview is reloading, and remove on failure.
 890          api.preview.bind( 'loading-initiated', function () {
 891              $( 'body' ).addClass( 'wp-customizer-unloading' );
 892          });
 893          api.preview.bind( 'loading-failed', function () {
 894              $( 'body' ).removeClass( 'wp-customizer-unloading' );
 895          });
 896  
 897          /* Custom Backgrounds */
 898          bg = $.map( ['color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment'], function( prop ) {
 899              return 'background_' + prop;
 900          } );
 901  
 902          api.when.apply( api, bg ).done( function( ...settings ) {
 903              $.each( settings, function() {
 904                  this.bind( api.settingPreviewHandlers.background );
 905              });
 906          });
 907  
 908          /**
 909           * Custom Logo
 910           *
 911           * Toggle the wp-custom-logo body class when a logo is added or removed.
 912           *
 913           * @since 4.5.0
 914           */
 915          api( 'custom_logo', function ( setting ) {
 916              api.settingPreviewHandlers.custom_logo.call( setting, setting.get() );
 917              setting.bind( api.settingPreviewHandlers.custom_logo );
 918          } );
 919  
 920          api( 'custom_css[' + api.settings.theme.stylesheet + ']', function( setting ) {
 921              setting.bind( api.settingPreviewHandlers.custom_css );
 922          } );
 923  
 924          api.trigger( 'preview-ready' );
 925      });
 926  
 927  })( wp, jQuery );


Generated : Fri Sep 11 08:20:31 2026 Cross-referenced by PHPXref