ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

customize-preview.js (27960B)


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