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