Theme Customizer: Fix race condition in previewer and use message channels. Props koopersmith. fixes #20811
git-svn-id: http://core.svn.wordpress.org/trunk@20988 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
parent
bb905a7899
commit
e69299af51
|
@ -281,6 +281,116 @@
|
|||
// Create the collection of Control objects.
|
||||
api.control = new api.Values({ defaultConstructor: api.Control });
|
||||
|
||||
api.PreviewFrame = api.Messenger.extend({
|
||||
sensitivity: 2000,
|
||||
|
||||
initialize: function( params, options ) {
|
||||
var loaded = false,
|
||||
ready = false,
|
||||
deferred = $.Deferred(),
|
||||
self = this;
|
||||
|
||||
// This is the promise object.
|
||||
deferred.promise( this );
|
||||
|
||||
this.previewer = params.previewer;
|
||||
|
||||
$.extend( params, { channel: api.PreviewFrame.uuid() });
|
||||
|
||||
api.Messenger.prototype.initialize.call( this, params, options );
|
||||
|
||||
this.bind( 'ready', function() {
|
||||
ready = true;
|
||||
|
||||
if ( loaded )
|
||||
deferred.resolveWith( self );
|
||||
});
|
||||
|
||||
params.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
|
||||
|
||||
this.request = $.ajax( this.url(), {
|
||||
type: 'POST',
|
||||
data: params.query,
|
||||
xhrFields: {
|
||||
withCredentials: true
|
||||
}
|
||||
} );
|
||||
|
||||
this.request.fail( function() {
|
||||
deferred.rejectWith( self, [ 'request failure' ] );
|
||||
});
|
||||
|
||||
this.request.done( function( response ) {
|
||||
var location = self.request.getResponseHeader('Location'),
|
||||
signature = 'WP_CUSTOMIZER_SIGNATURE',
|
||||
index;
|
||||
|
||||
// Check if the location response header differs from the current URL.
|
||||
// If so, the request was redirected; try loading the requested page.
|
||||
if ( location && location != self.url() ) {
|
||||
deferred.rejectWith( self, [ 'redirect', location ] );
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for a signature in the request.
|
||||
index = response.lastIndexOf( signature );
|
||||
if ( -1 === index || index < response.lastIndexOf('</html>') ) {
|
||||
deferred.rejectWith( self, [ 'unsigned' ] );
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip the signature from the request.
|
||||
response = response.slice( 0, index ) + response.slice( index + signature.length );
|
||||
|
||||
// Create the iframe and inject the html content.
|
||||
// Strip the signature from the request.
|
||||
response = response.slice( 0, index ) + response.slice( index + signature.length );
|
||||
|
||||
// Create the iframe and inject the html content.
|
||||
self.iframe = $('<iframe />').appendTo( self.previewer.container );
|
||||
|
||||
// Bind load event after the iframe has been added to the page;
|
||||
// otherwise it will fire when injected into the DOM.
|
||||
self.iframe.one( 'load', function() {
|
||||
loaded = true;
|
||||
|
||||
if ( ready ) {
|
||||
deferred.resolveWith( self );
|
||||
} else {
|
||||
setTimeout( function() {
|
||||
deferred.rejectWith( self, [ 'ready timeout' ] );
|
||||
}, self.sensitivity );
|
||||
}
|
||||
});
|
||||
|
||||
self.targetWindow( self.iframe[0].contentWindow );
|
||||
|
||||
self.targetWindow().document.open();
|
||||
self.targetWindow().document.write( response );
|
||||
self.targetWindow().document.close();
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
api.Messenger.prototype.destroy.call( this );
|
||||
this.request.abort();
|
||||
|
||||
if ( this.iframe )
|
||||
this.iframe.remove();
|
||||
|
||||
delete this.request;
|
||||
delete this.iframe;
|
||||
delete this.targetWindow;
|
||||
}
|
||||
});
|
||||
|
||||
(function(){
|
||||
var uuid = 0;
|
||||
api.PreviewFrame.uuid = function() {
|
||||
return 'preview-' + uuid++;
|
||||
};
|
||||
}());
|
||||
|
||||
api.Previewer = api.Messenger.extend({
|
||||
refreshBuffer: 250,
|
||||
|
||||
|
@ -295,8 +405,6 @@
|
|||
|
||||
$.extend( this, options || {} );
|
||||
|
||||
this.loaded = $.proxy( this.loaded, this );
|
||||
|
||||
/*
|
||||
* Wrap this.refresh to prevent it from hammering the servers:
|
||||
*
|
||||
|
@ -320,9 +428,7 @@
|
|||
return function() {
|
||||
if ( typeof timeout !== 'number' ) {
|
||||
if ( self.loading ) {
|
||||
self.loading.remove();
|
||||
delete self.loading;
|
||||
self.loader();
|
||||
self.abort();
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
|
@ -336,7 +442,7 @@
|
|||
this.container = api.ensure( params.container );
|
||||
this.allowedUrls = params.allowedUrls;
|
||||
|
||||
api.Messenger.prototype.initialize.call( this, params.url );
|
||||
api.Messenger.prototype.initialize.call( this, params );
|
||||
|
||||
// We're dynamically generating the iframe, so the origin is set
|
||||
// to the current window's location, not the url's.
|
||||
|
@ -391,65 +497,49 @@
|
|||
// Update the URL when the iframe sends a URL message.
|
||||
this.bind( 'url', this.url );
|
||||
},
|
||||
loader: function() {
|
||||
if ( this.loading )
|
||||
return this.loading;
|
||||
|
||||
this.loading = $('<iframe />').appendTo( this.container );
|
||||
|
||||
return this.loading;
|
||||
},
|
||||
loaded: function() {
|
||||
if ( this.iframe )
|
||||
this.iframe.remove();
|
||||
|
||||
this.iframe = this.loading;
|
||||
delete this.loading;
|
||||
|
||||
this.targetWindow( this.iframe[0].contentWindow );
|
||||
this.send( 'scroll', this.scroll );
|
||||
},
|
||||
query: function() {},
|
||||
|
||||
abort: function() {
|
||||
if ( this.loading ) {
|
||||
this.loading.destroy();
|
||||
delete this.loading;
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
var self = this;
|
||||
|
||||
if ( this.request )
|
||||
this.request.abort();
|
||||
this.abort();
|
||||
|
||||
this.request = $.ajax( this.url(), {
|
||||
type: 'POST',
|
||||
data: this.query() || {},
|
||||
success: function( response ) {
|
||||
var iframe = self.loader()[0].contentWindow,
|
||||
location = self.request.getResponseHeader('Location'),
|
||||
signature = 'WP_CUSTOMIZER_SIGNATURE',
|
||||
index;
|
||||
this.loading = new api.PreviewFrame({
|
||||
url: this.url(),
|
||||
query: this.query() || {},
|
||||
previewer: this
|
||||
});
|
||||
|
||||
// Check if the location response header differs from the current URL.
|
||||
// If so, the request was redirected; try loading the requested page.
|
||||
if ( location && location != self.url() ) {
|
||||
self.url( location );
|
||||
return;
|
||||
}
|
||||
this.loading.done( function() {
|
||||
// 'this' is the loading frame
|
||||
this.bind( 'synced', function() {
|
||||
if ( self.iframe )
|
||||
self.iframe.destroy();
|
||||
self.iframe = this;
|
||||
delete self.loading;
|
||||
|
||||
// Check for a signature in the request.
|
||||
index = response.lastIndexOf( signature );
|
||||
if ( -1 === index || index < response.lastIndexOf('</html>') )
|
||||
return;
|
||||
self.targetWindow( this.targetWindow() );
|
||||
self.channel( this.channel() );
|
||||
});
|
||||
|
||||
// Strip the signature from the request.
|
||||
response = response.slice( 0, index ) + response.slice( index + signature.length );
|
||||
this.send( 'sync', {
|
||||
scroll: self.scroll,
|
||||
settings: api.get()
|
||||
});
|
||||
});
|
||||
|
||||
self.loader().one( 'load', self.loaded );
|
||||
|
||||
iframe.document.open();
|
||||
iframe.document.write( response );
|
||||
iframe.document.close();
|
||||
},
|
||||
xhrFields: {
|
||||
withCredentials: true
|
||||
}
|
||||
} );
|
||||
this.loading.fail( function( reason, location ) {
|
||||
if ( 'redirect' === reason && location )
|
||||
self.url( location );
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -617,7 +707,10 @@
|
|||
});
|
||||
|
||||
// Create a potential postMessage connection with the parent frame.
|
||||
parent = new api.Messenger( api.settings.url.parent );
|
||||
parent = new api.Messenger({
|
||||
url: api.settings.url.parent,
|
||||
channel: 'loader'
|
||||
});
|
||||
|
||||
// If we receive a 'back' event, we're inside an iframe.
|
||||
// Send any clicks to the 'Return' link to the parent page.
|
||||
|
|
|
@ -290,7 +290,8 @@ final class WP_Customize_Manager {
|
|||
*/
|
||||
public function customize_preview_settings() {
|
||||
$settings = array(
|
||||
'values' => array(),
|
||||
'values' => array(),
|
||||
'channel' => esc_js( $_POST['customize_messenger_channel'] ),
|
||||
);
|
||||
|
||||
foreach ( $this->settings as $id => $setting ) {
|
||||
|
|
|
@ -302,13 +302,12 @@ if ( typeof wp === 'undefined' )
|
|||
return this.add( id, new this.defaultConstructor( api.Class.applicator, slice.call( arguments, 1 ) ) );
|
||||
},
|
||||
|
||||
get: function() {
|
||||
var result = {};
|
||||
each: function( callback, context ) {
|
||||
context = typeof context === 'undefined' ? this : context;
|
||||
|
||||
$.each( this._value, function( key, obj ) {
|
||||
result[ key ] = obj.get();
|
||||
} );
|
||||
return result;
|
||||
callback.call( context, obj, key );
|
||||
});
|
||||
},
|
||||
|
||||
remove: function( id ) {
|
||||
|
@ -481,19 +480,36 @@ if ( typeof wp === 'undefined' )
|
|||
return this[ key ] = new api.Value( initial, options );
|
||||
},
|
||||
|
||||
initialize: function( url, targetWindow, options ) {
|
||||
/**
|
||||
* Initialize Messenger.
|
||||
*
|
||||
* @param {object} params Parameters to configure the messenger.
|
||||
* {string} .url The URL to communicate with.
|
||||
* {window} .targetWindow The window instance to communicate with. Default window.parent.
|
||||
* {string} .channel If provided, will send the channel with each message and only accept messages a matching channel.
|
||||
* @param {object} options Extend any instance parameter or method with this object.
|
||||
*/
|
||||
initialize: function( params, options ) {
|
||||
// Target the parent frame by default, but only if a parent frame exists.
|
||||
var defaultTarget = window.parent == window ? null : window.parent;
|
||||
|
||||
$.extend( this, options || {} );
|
||||
|
||||
url = this.add( 'url', url );
|
||||
this.add( 'targetWindow', targetWindow || defaultTarget );
|
||||
this.add( 'origin', url() ).link( url ).setter( function( to ) {
|
||||
this.add( 'channel', params.channel );
|
||||
this.add( 'url', params.url );
|
||||
this.add( 'targetWindow', params.targetWindow || defaultTarget );
|
||||
this.add( 'origin', this.url() ).link( this.url ).setter( function( to ) {
|
||||
return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' );
|
||||
});
|
||||
|
||||
// Since we want jQuery to treat the receive function as unique
|
||||
// to this instance, we give the function a new guid.
|
||||
//
|
||||
// This will prevent every Messenger's receive function from being
|
||||
// unbound when calling $.off( 'message', this.receive );
|
||||
this.receive = $.proxy( this.receive, this );
|
||||
this.receive.guid = $.guid++;
|
||||
|
||||
$( window ).on( 'message', this.receive );
|
||||
},
|
||||
|
||||
|
@ -515,8 +531,15 @@ if ( typeof wp === 'undefined' )
|
|||
|
||||
message = JSON.parse( event.data );
|
||||
|
||||
if ( message && message.id && typeof message.data !== 'undefined' )
|
||||
this.trigger( message.id, message.data );
|
||||
// Check required message properties.
|
||||
if ( ! message || ! message.id || typeof message.data === 'undefined' )
|
||||
return;
|
||||
|
||||
// Check if channel names match.
|
||||
if ( ( message.channel || this.channel() ) && this.channel() !== message.channel )
|
||||
return;
|
||||
|
||||
this.trigger( message.id, message.data );
|
||||
},
|
||||
|
||||
send: function( id, data ) {
|
||||
|
@ -527,8 +550,11 @@ if ( typeof wp === 'undefined' )
|
|||
if ( ! this.url() || ! this.targetWindow() )
|
||||
return;
|
||||
|
||||
message = JSON.stringify({ id: id, data: data });
|
||||
this.targetWindow().postMessage( message, this.origin() );
|
||||
message = { id: id, data: data };
|
||||
if ( this.channel() )
|
||||
message.channel = this.channel();
|
||||
|
||||
this.targetWindow().postMessage( JSON.stringify( message ), this.origin() );
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -540,6 +566,15 @@ if ( typeof wp === 'undefined' )
|
|||
* ===================================================================== */
|
||||
|
||||
api = $.extend( new api.Values(), api );
|
||||
api.get = function() {
|
||||
var result = {};
|
||||
|
||||
this.each( function( obj, key ) {
|
||||
result[ key ] = obj.get();
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Expose the API to the world.
|
||||
exports.customize = api;
|
||||
|
|
|
@ -77,7 +77,11 @@ if ( typeof wp === 'undefined' )
|
|||
this.iframe.one( 'load', this.loaded );
|
||||
|
||||
// Create a postMessage connection with the iframe.
|
||||
this.messenger = new api.Messenger( src, this.iframe[0].contentWindow );
|
||||
this.messenger = new api.Messenger({
|
||||
url: src,
|
||||
channel: 'loader',
|
||||
targetWindow: this.iframe[0].contentWindow
|
||||
});
|
||||
|
||||
// Wait for the connection from the iframe before sending any postMessage events.
|
||||
this.messenger.bind( 'ready', function() {
|
||||
|
|
|
@ -21,15 +21,11 @@
|
|||
/**
|
||||
* Requires params:
|
||||
* - url - the URL of preview frame
|
||||
*
|
||||
* @todo: Perhaps add a window.onbeforeunload dialog in case the theme
|
||||
* somehow attempts to leave the page and we don't catch it
|
||||
* (which really shouldn't happen).
|
||||
*/
|
||||
initialize: function( url, options ) {
|
||||
initialize: function( params, options ) {
|
||||
var self = this;
|
||||
|
||||
api.Messenger.prototype.initialize.call( this, url, null, options );
|
||||
api.Messenger.prototype.initialize.call( this, params, options );
|
||||
|
||||
this.body = $( document.body );
|
||||
this.body.on( 'click.preview', 'a', function( event ) {
|
||||
|
@ -39,8 +35,7 @@
|
|||
});
|
||||
|
||||
// You cannot submit forms.
|
||||
// @todo: Namespace customizer settings so that we can mix the
|
||||
// $_POST data with the customize setting $_POST data.
|
||||
// @todo: Allow form submissions by mixing $_POST data with the customize setting $_POST data.
|
||||
this.body.on( 'submit.preview', 'form', function( event ) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
@ -63,18 +58,40 @@
|
|||
|
||||
var preview, bg;
|
||||
|
||||
preview = new api.Preview( window.location.href );
|
||||
|
||||
$.each( api.settings.values, function( id, value ) {
|
||||
api.create( id, value );
|
||||
preview = new api.Preview({
|
||||
url: window.location.href,
|
||||
channel: api.settings.channel
|
||||
});
|
||||
|
||||
preview.bind( 'settings', function( values ) {
|
||||
$.each( values, function( id, value ) {
|
||||
if ( api.has( id ) )
|
||||
api( id ).set( value );
|
||||
else
|
||||
api.create( id, value );
|
||||
});
|
||||
});
|
||||
|
||||
preview.trigger( 'settings', api.settings.values );
|
||||
|
||||
preview.bind( 'setting', function( args ) {
|
||||
var value = api( args.shift() );
|
||||
if ( value )
|
||||
var value;
|
||||
|
||||
args = args.slice();
|
||||
|
||||
if ( value = api( args.shift() ) )
|
||||
value.set.apply( value, args );
|
||||
});
|
||||
|
||||
preview.bind( 'sync', function( events ) {
|
||||
$.each( events, function( event, args ) {
|
||||
preview.trigger( event, args );
|
||||
});
|
||||
preview.send( 'synced' );
|
||||
})
|
||||
|
||||
preview.send( 'ready' );
|
||||
|
||||
/* Custom Backgrounds */
|
||||
bg = $.map(['color', 'image', 'position_x', 'repeat', 'attachment'], function( prop ) {
|
||||
return 'background_' + prop;
|
||||
|
|
Loading…
Reference in New Issue