| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Sep 11 08:20:31 2026 | Cross-referenced by PHPXref |