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