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.
|
// Create the collection of Control objects.
|
||||||
api.control = new api.Values({ defaultConstructor: api.Control });
|
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({
|
api.Previewer = api.Messenger.extend({
|
||||||
refreshBuffer: 250,
|
refreshBuffer: 250,
|
||||||
|
|
||||||
|
@ -295,8 +405,6 @@
|
||||||
|
|
||||||
$.extend( this, options || {} );
|
$.extend( this, options || {} );
|
||||||
|
|
||||||
this.loaded = $.proxy( this.loaded, this );
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Wrap this.refresh to prevent it from hammering the servers:
|
* Wrap this.refresh to prevent it from hammering the servers:
|
||||||
*
|
*
|
||||||
|
@ -320,9 +428,7 @@
|
||||||
return function() {
|
return function() {
|
||||||
if ( typeof timeout !== 'number' ) {
|
if ( typeof timeout !== 'number' ) {
|
||||||
if ( self.loading ) {
|
if ( self.loading ) {
|
||||||
self.loading.remove();
|
self.abort();
|
||||||
delete self.loading;
|
|
||||||
self.loader();
|
|
||||||
} else {
|
} else {
|
||||||
return callback();
|
return callback();
|
||||||
}
|
}
|
||||||
|
@ -336,7 +442,7 @@
|
||||||
this.container = api.ensure( params.container );
|
this.container = api.ensure( params.container );
|
||||||
this.allowedUrls = params.allowedUrls;
|
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
|
// We're dynamically generating the iframe, so the origin is set
|
||||||
// to the current window's location, not the url's.
|
// to the current window's location, not the url's.
|
||||||
|
@ -391,65 +497,49 @@
|
||||||
// Update the URL when the iframe sends a URL message.
|
// Update the URL when the iframe sends a URL message.
|
||||||
this.bind( 'url', this.url );
|
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() {},
|
query: function() {},
|
||||||
|
|
||||||
|
abort: function() {
|
||||||
|
if ( this.loading ) {
|
||||||
|
this.loading.destroy();
|
||||||
|
delete this.loading;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
refresh: function() {
|
refresh: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if ( this.request )
|
this.abort();
|
||||||
this.request.abort();
|
|
||||||
|
|
||||||
this.request = $.ajax( this.url(), {
|
this.loading = new api.PreviewFrame({
|
||||||
type: 'POST',
|
url: this.url(),
|
||||||
data: this.query() || {},
|
query: this.query() || {},
|
||||||
success: function( response ) {
|
previewer: this
|
||||||
var iframe = self.loader()[0].contentWindow,
|
});
|
||||||
location = self.request.getResponseHeader('Location'),
|
|
||||||
signature = 'WP_CUSTOMIZER_SIGNATURE',
|
|
||||||
index;
|
|
||||||
|
|
||||||
// Check if the location response header differs from the current URL.
|
this.loading.done( function() {
|
||||||
// If so, the request was redirected; try loading the requested page.
|
// 'this' is the loading frame
|
||||||
if ( location && location != self.url() ) {
|
this.bind( 'synced', function() {
|
||||||
self.url( location );
|
if ( self.iframe )
|
||||||
return;
|
self.iframe.destroy();
|
||||||
}
|
self.iframe = this;
|
||||||
|
delete self.loading;
|
||||||
|
|
||||||
// Check for a signature in the request.
|
self.targetWindow( this.targetWindow() );
|
||||||
index = response.lastIndexOf( signature );
|
self.channel( this.channel() );
|
||||||
if ( -1 === index || index < response.lastIndexOf('</html>') )
|
});
|
||||||
return;
|
|
||||||
|
|
||||||
// Strip the signature from the request.
|
this.send( 'sync', {
|
||||||
response = response.slice( 0, index ) + response.slice( index + signature.length );
|
scroll: self.scroll,
|
||||||
|
settings: api.get()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
self.loader().one( 'load', self.loaded );
|
this.loading.fail( function( reason, location ) {
|
||||||
|
if ( 'redirect' === reason && location )
|
||||||
iframe.document.open();
|
self.url( location );
|
||||||
iframe.document.write( response );
|
});
|
||||||
iframe.document.close();
|
|
||||||
},
|
|
||||||
xhrFields: {
|
|
||||||
withCredentials: true
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -617,7 +707,10 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a potential postMessage connection with the parent frame.
|
// 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.
|
// If we receive a 'back' event, we're inside an iframe.
|
||||||
// Send any clicks to the 'Return' link to the parent page.
|
// 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() {
|
public function customize_preview_settings() {
|
||||||
$settings = array(
|
$settings = array(
|
||||||
'values' => array(),
|
'values' => array(),
|
||||||
|
'channel' => esc_js( $_POST['customize_messenger_channel'] ),
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $this->settings as $id => $setting ) {
|
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 ) ) );
|
return this.add( id, new this.defaultConstructor( api.Class.applicator, slice.call( arguments, 1 ) ) );
|
||||||
},
|
},
|
||||||
|
|
||||||
get: function() {
|
each: function( callback, context ) {
|
||||||
var result = {};
|
context = typeof context === 'undefined' ? this : context;
|
||||||
|
|
||||||
$.each( this._value, function( key, obj ) {
|
$.each( this._value, function( key, obj ) {
|
||||||
result[ key ] = obj.get();
|
callback.call( context, obj, key );
|
||||||
} );
|
});
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
remove: function( id ) {
|
remove: function( id ) {
|
||||||
|
@ -481,19 +480,36 @@ if ( typeof wp === 'undefined' )
|
||||||
return this[ key ] = new api.Value( initial, options );
|
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.
|
// Target the parent frame by default, but only if a parent frame exists.
|
||||||
var defaultTarget = window.parent == window ? null : window.parent;
|
var defaultTarget = window.parent == window ? null : window.parent;
|
||||||
|
|
||||||
$.extend( this, options || {} );
|
$.extend( this, options || {} );
|
||||||
|
|
||||||
url = this.add( 'url', url );
|
this.add( 'channel', params.channel );
|
||||||
this.add( 'targetWindow', targetWindow || defaultTarget );
|
this.add( 'url', params.url );
|
||||||
this.add( 'origin', url() ).link( url ).setter( function( to ) {
|
this.add( 'targetWindow', params.targetWindow || defaultTarget );
|
||||||
|
this.add( 'origin', this.url() ).link( this.url ).setter( function( to ) {
|
||||||
return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' );
|
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 = $.proxy( this.receive, this );
|
||||||
|
this.receive.guid = $.guid++;
|
||||||
|
|
||||||
$( window ).on( 'message', this.receive );
|
$( window ).on( 'message', this.receive );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -515,8 +531,15 @@ if ( typeof wp === 'undefined' )
|
||||||
|
|
||||||
message = JSON.parse( event.data );
|
message = JSON.parse( event.data );
|
||||||
|
|
||||||
if ( message && message.id && typeof message.data !== 'undefined' )
|
// Check required message properties.
|
||||||
this.trigger( message.id, message.data );
|
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 ) {
|
send: function( id, data ) {
|
||||||
|
@ -527,8 +550,11 @@ if ( typeof wp === 'undefined' )
|
||||||
if ( ! this.url() || ! this.targetWindow() )
|
if ( ! this.url() || ! this.targetWindow() )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
message = JSON.stringify({ id: id, data: data });
|
message = { id: id, data: data };
|
||||||
this.targetWindow().postMessage( message, this.origin() );
|
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 = $.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.
|
// Expose the API to the world.
|
||||||
exports.customize = api;
|
exports.customize = api;
|
||||||
|
|
|
@ -77,7 +77,11 @@ if ( typeof wp === 'undefined' )
|
||||||
this.iframe.one( 'load', this.loaded );
|
this.iframe.one( 'load', this.loaded );
|
||||||
|
|
||||||
// Create a postMessage connection with the iframe.
|
// 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.
|
// Wait for the connection from the iframe before sending any postMessage events.
|
||||||
this.messenger.bind( 'ready', function() {
|
this.messenger.bind( 'ready', function() {
|
||||||
|
|
|
@ -21,15 +21,11 @@
|
||||||
/**
|
/**
|
||||||
* Requires params:
|
* Requires params:
|
||||||
* - url - the URL of preview frame
|
* - 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;
|
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 = $( document.body );
|
||||||
this.body.on( 'click.preview', 'a', function( event ) {
|
this.body.on( 'click.preview', 'a', function( event ) {
|
||||||
|
@ -39,8 +35,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// You cannot submit forms.
|
// You cannot submit forms.
|
||||||
// @todo: Namespace customizer settings so that we can mix the
|
// @todo: Allow form submissions by mixing $_POST data with the customize setting $_POST data.
|
||||||
// $_POST data with the customize setting $_POST data.
|
|
||||||
this.body.on( 'submit.preview', 'form', function( event ) {
|
this.body.on( 'submit.preview', 'form', function( event ) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
@ -63,18 +58,40 @@
|
||||||
|
|
||||||
var preview, bg;
|
var preview, bg;
|
||||||
|
|
||||||
preview = new api.Preview( window.location.href );
|
preview = new api.Preview({
|
||||||
|
url: window.location.href,
|
||||||
$.each( api.settings.values, function( id, value ) {
|
channel: api.settings.channel
|
||||||
api.create( id, value );
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 ) {
|
preview.bind( 'setting', function( args ) {
|
||||||
var value = api( args.shift() );
|
var value;
|
||||||
if ( value )
|
|
||||||
|
args = args.slice();
|
||||||
|
|
||||||
|
if ( value = api( args.shift() ) )
|
||||||
value.set.apply( value, args );
|
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 */
|
/* Custom Backgrounds */
|
||||||
bg = $.map(['color', 'image', 'position_x', 'repeat', 'attachment'], function( prop ) {
|
bg = $.map(['color', 'image', 'position_x', 'repeat', 'attachment'], function( prop ) {
|
||||||
return 'background_' + prop;
|
return 'background_' + prop;
|
||||||
|
|
Loading…
Reference in New Issue