* Script run inside a Customizer preview frame.
* @output wp-includes/js/customize-preview.js
currentHistoryState = {};
* Capture the state that is passed into history.replaceState() and history.pushState()
* and also which is returned in the popstate event so that when the changeset_uuid
* gets updated when transitioning to a new changeset there the current state will
* be supplied in the call to history.replaceState().
if ( ! history.replaceState ) {
* Amend the supplied URL with the customized state.
* @param {string} url URL.
* @return {string} URL with customized state.
injectUrlWithState = function( url ) {
var urlParser, oldQueryParams, newQueryParams;
urlParser = document.createElement( 'a' );
oldQueryParams = api.utils.parseQueryString( location.search.substr( 1 ) );
newQueryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
newQueryParams.customize_changeset_uuid = oldQueryParams.customize_changeset_uuid;
if ( oldQueryParams.customize_autosaved ) {
newQueryParams.customize_autosaved = 'on';
if ( oldQueryParams.customize_theme ) {
newQueryParams.customize_theme = oldQueryParams.customize_theme;
if ( oldQueryParams.customize_messenger_channel ) {
newQueryParams.customize_messenger_channel = oldQueryParams.customize_messenger_channel;
urlParser.search = $.param( newQueryParams );
history.replaceState = ( function( nativeReplaceState ) {
return function historyReplaceState( data, title, url ) {
currentHistoryState = data;
return nativeReplaceState.call( history, data, title, 'string' === typeof url && url.length > 0 ? injectUrlWithState( url ) : url );
} )( history.replaceState );
history.pushState = ( function( nativePushState ) {
return function historyPushState( data, title, url ) {
currentHistoryState = data;
return nativePushState.call( history, data, title, 'string' === typeof url && url.length > 0 ? injectUrlWithState( url ) : url );
} )( history.pushState );
window.addEventListener( 'popstate', function( event ) {
currentHistoryState = event.state;
* Returns a debounced version of the function.
* @todo Require Underscore.js for this file and retire this.
debounce = function( fn, delay, context ) {
context = context || this;
timeout = setTimeout( function() {
fn.apply( context, args );
* @alias wp.customize.Preview
* @augments wp.customize.Messenger
* @augments wp.customize.Class
* @mixes wp.customize.Events
api.Preview = api.Messenger.extend(/** @lends wp.customize.Preview.prototype */{
* @param {Object} params - Parameters to configure the messenger.
* @param {Object} options - Extend any instance parameter or method with this object.
initialize: function( params, options ) {
var preview = this, urlParser = document.createElement( 'a' );
api.Messenger.prototype.initialize.call( preview, params, options );
urlParser.href = preview.origin();
preview.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
preview.body = $( document.body );
preview.window = $( window );
if ( api.settings.channel ) {
// If in an iframe, then intercept the link clicks and form submissions.
preview.body.on( 'click.preview', 'a', function( event ) {
preview.handleLinkClick( event );
preview.body.on( 'submit.preview', 'form', function( event ) {
preview.handleFormSubmit( event );
preview.window.on( 'scroll.preview', debounce( function() {
preview.send( 'scroll', preview.window.scrollTop() );
preview.bind( 'scroll', function( distance ) {
preview.window.scrollTop( distance );
* Handle link clicks in preview.
* @param {jQuery.Event} event Event.
handleLinkClick: function( event ) {
var preview = this, link, isInternalJumpLink;
link = $( event.target ).closest( 'a' );
// No-op if the anchor is not a link.
if ( _.isUndefined( link.attr( 'href' ) ) ) {
// Allow internal jump links and JS links to behave normally without preventing default.
isInternalJumpLink = ( '#' === link.attr( 'href' ).substr( 0, 1 ) );
if ( isInternalJumpLink || ! /^https?:$/.test( link.prop( 'protocol' ) ) ) {
// If the link is not previewable, prevent the browser from navigating to it.
if ( ! api.isLinkPreviewable( link[0] ) ) {
wp.a11y.speak( api.settings.l10n.linkUnpreviewable );
// Prevent initiating navigating from click and instead rely on sending url message to pane.
* Note the shift key is checked so shift+click on widgets or
* nav menu items can just result on focusing on the corresponding
* control instead of also navigating to the URL linked to.
// Note: It's not relevant to send scroll because sending url message will have the same effect.
preview.send( 'url', link.prop( 'href' ) );
* @param {jQuery.Event} event Event.
handleFormSubmit: function( event ) {
var preview = this, urlParser, form;
urlParser = document.createElement( 'a' );
form = $( event.target );
urlParser.href = form.prop( 'action' );
// If the link is not previewable, prevent the browser from navigating to it.
if ( 'GET' !== form.prop( 'method' ).toUpperCase() || ! api.isLinkPreviewable( urlParser ) ) {
wp.a11y.speak( api.settings.l10n.formUnpreviewable );
* If the default wasn't prevented already (in which case the form
* submission is already being handled by JS), and if it has a GET
* request method, then take the serialized form data and add it as
* a query string to the action URL and send this in a url message
* to the customizer pane so that it will be loaded. If the form's
* action points to a non-previewable URL, the customizer pane's
* previewUrl setter will reject it so that the form submission is
* a no-op, which is the same behavior as when clicking a link to an
* external site in the preview.
if ( ! event.isDefaultPrevented() ) {
if ( urlParser.search.length > 1 ) {
urlParser.search += form.serialize();
preview.send( 'url', urlParser.href );
// Prevent default since navigation should be done via sending url message or via JS submit handler.
* Inject the changeset UUID into links in the document.
api.addLinkPreviewing = function addLinkPreviewing() {
var linkSelectors = 'a[href], area[href]';
// Inject links into initial document.
$( document.body ).find( linkSelectors ).each( function() {
api.prepareLinkPreview( this );
// Inject links for new elements added to the page.
if ( 'undefined' !== typeof MutationObserver ) {
api.mutationObserver = new MutationObserver( function( mutations ) {
_.each( mutations, function( mutation ) {
$( mutation.target ).find( linkSelectors ).each( function() {
api.prepareLinkPreview( this );
api.mutationObserver.observe( document.documentElement, {
// If mutation observers aren't available, fallback to just-in-time injection.
$( document.documentElement ).on( 'click focus mouseover', linkSelectors, function() {
api.prepareLinkPreview( this );
* Should the supplied link is previewable.
* @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
* @param {string} element.search Query string.
* @param {string} element.pathname Path.
* @param {string} element.host Host.
* @param {Object} [options]
* @param {Object} [options.allowAdminAjax=false] Allow admin-ajax.php requests.
* @return {boolean} Is appropriate for changeset link.
api.isLinkPreviewable = function isLinkPreviewable( element, options ) {
var matchesAllowedUrl, parsedAllowedUrl, args, elementHost;
args = _.extend( {}, { allowAdminAjax: false }, options || {} );
if ( 'javascript:' === element.protocol ) { // jshint ignore:line
// Only web URLs can be previewed.
if ( 'https:' !== element.protocol && 'http:' !== element.protocol ) {
elementHost = element.host.replace( /:(80|443)$/, '' );
parsedAllowedUrl = document.createElement( 'a' );
matchesAllowedUrl = ! _.isUndefined( _.find( api.settings.url.allowed, function( allowedUrl ) {
parsedAllowedUrl.href = allowedUrl;
return parsedAllowedUrl.protocol === element.protocol && parsedAllowedUrl.host.replace( /:(80|443)$/, '' ) === elementHost && 0 === element.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) );
if ( ! matchesAllowedUrl ) {
// Skip wp login and signup pages.
if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) {
// Allow links to admin ajax as faux frontend URLs.
if ( /\/wp-admin\/admin-ajax\.php$/.test( element.pathname ) ) {
return args.allowAdminAjax;
// Disallow links to admin, includes, and content.
if ( /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) {
* Inject the customize_changeset_uuid query param into links on the frontend.
* @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
* @param {string} element.search Query string.
* @param {string} element.host Host.
* @param {string} element.protocol Protocol.
api.prepareLinkPreview = function prepareLinkPreview( element ) {
var queryParams, $element = $( element );
// Skip elements with no href attribute. Check first to avoid more expensive checks down the road.
if ( ! element.hasAttribute( 'href' ) ) {
// Skip links in admin bar.
if ( $element.closest( '#wpadminbar' ).length ) {
// Ignore links with href="#", href="#id", or non-HTTP protocols (e.g. javascript: and mailto:).
if ( '#' === $element.attr( 'href' ).substr( 0, 1 ) || ! /^https?:$/.test( element.protocol ) ) {
// Make sure links in preview use HTTPS if parent frame uses HTTPS.
if ( api.settings.channel && 'https' === api.preview.scheme.get() && 'http:' === element.protocol && -1 !== api.settings.url.allowedHosts.indexOf( element.host ) ) {
element.protocol = 'https:';
// Ignore links with class wp-playlist-caption.
if ( $element.hasClass( 'wp-playlist-caption' ) ) {
if ( ! api.isLinkPreviewable( element ) ) {
// Style link as unpreviewable only if previewing in iframe; if previewing on frontend, links will be allowed to work normally.
if ( api.settings.channel ) {
$element.addClass( 'customize-unpreviewable' );
$element.removeClass( 'customize-unpreviewable' );
queryParams = api.utils.parseQueryString( element.search.substring( 1 ) );
queryParams.customize_changeset_uuid = api.settings.changeset.uuid;
if ( api.settings.changeset.autosaved ) {
queryParams.customize_autosaved = 'on';
if ( ! api.settings.theme.active ) {
queryParams.customize_theme = api.settings.theme.stylesheet;
if ( api.settings.channel ) {
queryParams.customize_messenger_channel = api.settings.channel;
element.search = $.param( queryParams );
* Inject the changeset UUID into Ajax requests.
api.addRequestPreviewing = function addRequestPreviewing() {
* Rewrite Ajax requests to inject customizer state.
* @param {Object} options Options.
* @param {string} options.type Type.
* @param {string} options.url URL.
* @param {Object} originalOptions Original options.
* @param {XMLHttpRequest} xhr XHR.
var prefilterAjax = function( options, originalOptions, xhr ) {
var urlParser, queryParams, requestMethod, dirtyValues = {};
urlParser = document.createElement( 'a' );
urlParser.href = options.url;
// Abort if the request is not for this site.
if ( ! api.isLinkPreviewable( urlParser, { allowAdminAjax: true } ) ) {
queryParams = api.utils.parseQueryString( urlParser.search.substring( 1 ) );
// Note that _dirty flag will be cleared with changeset updates.
api.each( function( setting ) {
dirtyValues[ setting.id ] = setting.get();
if ( ! _.isEmpty( dirtyValues ) ) {
requestMethod = options.type.toUpperCase();
// Override underlying request method to ensure unsaved changes to changeset can be included (force Backbone.emulateHTTP).
if ( 'POST' !== requestMethod ) {
xhr.setRequestHeader( 'X-HTTP-Method-Override', requestMethod );
queryParams._method = requestMethod;
// Amend the post data with the customized values.
options.data += $.param( {
customized: JSON.stringify( dirtyValues )
// Include customized state query params in URL.
queryParams.customize_changeset_uuid = api.settings.changeset.uuid;
if ( api.settings.changeset.autosaved ) {
queryParams.customize_autosaved = 'on';
if ( ! api.settings.theme.active ) {
queryParams.customize_theme = api.settings.theme.stylesheet;
// Ensure preview nonce is included with every customized request, to allow post data to be read.
queryParams.customize_preview_nonce = api.settings.nonce.preview;
urlParser.search = $.param( queryParams );
options.url = urlParser.href;
$.ajaxPrefilter( prefilterAjax );
* Inject changeset UUID into forms, allowing preview to persist through submissions.
api.addFormPreviewing = function addFormPreviewing() {
// Inject inputs for forms in initial document.
$( document.body ).find( 'form' ).each( function() {
api.prepareFormPreview( this );
// Inject inputs for new forms added to the page.
if ( 'undefined' !== typeof MutationObserver ) {
api.mutationObserver = new MutationObserver( function( mutations ) {
_.each( mutations, function( mutation ) {
$( mutation.target ).find( 'form' ).each( function() {
api.prepareFormPreview( this );
api.mutationObserver.observe( document.documentElement, {
* Inject changeset into form inputs.