Theme Customizer: Migrate to an ajax-based solution for refreshing the preview and saving. see #20507, #19910.

* Use ajax-based saving, add saving indicator.
* Use ajax-based refreshing instead of form targets.
* Instead of using hidden inputs with prefixed names to track the canonical data, use the values stored in wp.customize. Encode the values as JSON before sending to avoid bugs with ids that contain square brackets (PHP mangles POST values with nested brackets).
* Use wp.customize.Previewer solely for the purpose of previewing; move the postMessage connection with the parent frame and other unrelated code snippets into the 'ready' handler.


git-svn-id: http://svn.automattic.com/wordpress/trunk@20645 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
koopersmith 2012-04-30 15:46:17 +00:00
parent 17ee4064b1
commit 2f2cb0926f
6 changed files with 136 additions and 98 deletions

View File

@ -21,9 +21,6 @@ class WP_Customize_Setting {
protected $id_data = array();
private $_post_value; // Cached, sanitized $_POST value.
// Prefix for $_POST values to prevent naming conflicts.
const name_prefix = 'customize_';
/**
* Constructor.
*
@ -121,16 +118,8 @@ class WP_Customize_Setting {
if ( isset( $this->_post_value ) )
return $this->_post_value;
$base = self::name_prefix . $this->id_data[ 'base' ];
$result = $this->manager->post_value( $this );
if ( ! isset( $_POST[ $base ] ) )
return $default;
$result = $this->multidimensional_get( $_POST[ $base ], $this->id_data[ 'keys' ] );
if ( ! isset( $result ) )
return $default;
$result = $this->sanitize( $result );
if ( isset( $result ) )
return $this->_post_value = $result;
else

View File

@ -17,6 +17,10 @@ final class WP_Customize {
protected $sections = array();
protected $controls = array();
protected $customized;
private $_post_values;
/**
* Constructor.
*
@ -31,6 +35,8 @@ final class WP_Customize {
add_action( 'admin_init', array( $this, 'admin_init' ) );
add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
add_action( 'customize_register', array( $this, 'register_controls' ) );
add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
@ -148,6 +154,24 @@ final class WP_Customize {
$this->customize_preview_init();
}
/**
* Decode the $_POST attribute used to override the WP_Customize_Setting values.
*
* @since 3.4.0
*/
public function post_value( $setting ) {
if ( ! isset( $this->_post_values ) ) {
if ( isset( $_POST['customized'] ) )
$this->_post_values = json_decode( stripslashes( $_POST['customized'] ), true );
else
$this->_post_values = false;
}
if ( isset( $this->_post_values[ $setting->id ] ) )
return $setting->sanitize( $this->_post_values[ $setting->id ] );
}
/**
* Print javascript settings.
*
@ -267,9 +291,6 @@ final class WP_Customize {
* @since 3.4.0
*/
public function admin_init() {
if ( isset( $_REQUEST['save_customize_controls'] ) )
$this->save();
if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) )
return;
@ -297,14 +318,14 @@ final class WP_Customize {
*/
public function save() {
if ( ! $this->is_preview() )
return;
die;
check_admin_referer( 'customize_controls' );
check_ajax_referer( 'customize_controls', 'nonce' );
// Do we have to switch themes?
if ( $this->get_stylesheet() != $this->original_stylesheet ) {
if ( ! current_user_can( 'switch_themes' ) )
return;
die;
// Temporarily stop previewing the theme to allow switch_themes()
// to operate properly.
@ -320,6 +341,8 @@ final class WP_Customize {
}
add_action( 'admin_notices', array( $this, '_save_feedback' ) );
die;
}
/**

View File

@ -128,6 +128,16 @@ body {
margin: 0;
}
#customize-footer-actions img {
display: none;
position: absolute;
top: 18px;
margin-left: 4px;
}
.saving #customize-footer-actions img {
display: inline;
}
.customize-control {
float: left;
clear: both;

View File

@ -41,10 +41,8 @@ do_action( 'customize_controls_print_scripts' );
?>
</head>
<body class="wp-full-overlay">
<form id="customize-controls" method="post" class="wrap wp-full-overlay-sidebar" target="_parent" action="<?php echo esc_url( add_query_arg( 'save_customize_controls', '1', admin_url( 'themes.php' ) ) ); ?>">
<form id="customize-controls" class="wrap wp-full-overlay-sidebar">
<?php wp_nonce_field( 'customize_controls' ); ?>
<input type="hidden" name="customize" value="on" />
<input type="hidden" name="theme" value="<?php echo esc_attr( $this->get_stylesheet() ); ?>" />
<div id="customize-header-actions" class="customize-section wp-full-overlay-header">
<a class="back" href="<?php echo esc_url( admin_url( 'themes.php' ) ); ?>">
<?php printf( __( '&larr; Return to %s' ), __('Manage Themes') ); ?>
@ -79,15 +77,15 @@ do_action( 'customize_controls_print_scripts' );
$save_text = $this->get_stylesheet() == $this->original_stylesheet ? __('Save') : __('Save and Activate');
submit_button( $save_text, 'primary', 'save', false );
?>
<img src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" />
<a href="#" class="collapse-sidebar button-secondary" title="<?php esc_attr_e('Collapse Sidebar'); ?>">
<span class="collapse-sidebar-label"><?php _e('Collapse'); ?></span>
<span class="collapse-sidebar-arrow"></span>
</a>
</div>
</form>
<div id="customize-preview" class="wp-full-overlay-main">
<iframe name="customize-target"></iframe>
</div>
<div id="customize-preview" class="wp-full-overlay-main"></div>
<?php
do_action( 'customize_controls_print_footer_scripts' );
@ -95,11 +93,12 @@ do_action( 'customize_controls_print_scripts' );
// Check current scheme and load the preview with the same scheme
$scheme = is_ssl() ? 'https' : 'http';
$settings = array(
'theme' => $this->get_stylesheet(),
'preview' => esc_url( home_url( '/', $scheme ) ),
'settings' => array(),
'controls' => array(),
'prefix' => WP_Customize_Setting::name_prefix,
'parent' => esc_url( admin_url() ),
'ajax' => esc_url( admin_url( 'admin-ajax.php', 'relative' ) ),
);
foreach ( $this->settings as $id => $setting ) {

View File

@ -265,6 +265,18 @@ if ( typeof wp === 'undefined' )
return this._value[ id ];
},
get: function() {
var result = {};
if ( arguments.length )
return this.pass( 'get', arguments );
$.each( this._value, function( key, obj ) {
result[ key ] = obj.get();
} );
return result;
},
set: function( id ) {
if ( this.has( id ) )
return this.pass( 'set', arguments );
@ -326,7 +338,7 @@ if ( typeof wp === 'undefined' )
}
});
$.each( [ 'get', 'bind', 'unbind', 'link', 'unlink', 'sync', 'unsync', 'setter', 'resetSetter' ], function( i, method ) {
$.each( [ 'bind', 'unbind', 'link', 'unlink', 'sync', 'unsync', 'setter', 'resetSetter' ], function( i, method ) {
api.Values.prototype[ method ] = function() {
return this.pass( method, arguments );
};

View File

@ -15,16 +15,6 @@
this.id = id;
this.transport = this.transport || 'refresh';
element = $( '<input />', {
type: 'hidden',
value: this.get(),
name: api.settings.prefix + id
});
element.appendTo( this.previewer.form );
this.element = new api.Element( element );
this.sync( this.element );
this.bind( this.preview );
},
preview: function() {
@ -271,9 +261,8 @@
/**
* Requires params:
* - iframe - a selector or jQuery element
* - form - a selector or jQuery element
* - url - the URL of preview frame
* - container - a selector or jQuery element
* - url - the URL of preview frame
*/
initialize: function( params, options ) {
var self = this;
@ -282,8 +271,6 @@
this.loaded = $.proxy( this.loaded, this );
this.loaderUuid = 0;
/*
* Wrap this.refresh to prevent it from hammering the servers:
*
@ -320,82 +307,51 @@
};
})( this );
this.iframe = api.ensure( params.iframe );
this.form = api.ensure( params.form );
this.name = this.iframe.prop('name');
this.container = api.ensure( params.container );
this.container = this.iframe.parent();
api.Messenger.prototype.initialize.call( this, params.url, this.iframe[0].contentWindow );
this._formOriginalProps = {
target: this.form.prop('target'),
action: this.form.prop('action')
};
api.Messenger.prototype.initialize.call( this, params.url );
this.bind( 'url', function( url ) {
// Bail if we're navigating to the current url, to a different origin, or wp-admin.
if ( this.url() == url || 0 !== url.indexOf( this.origin() + '/' ) || -1 !== url.indexOf( 'wp-admin' ) )
if ( this.url() == url || 0 !== url.indexOf( this.origin() + '/' ) || -1 !== url.indexOf( 'wp-admin' ) )
return;
this.url( url );
this.refresh();
});
this.refresh();
// Prevent the form from saving when enter is pressed.
this.form.on( 'keydown', function( e ) {
if ( 13 === e.which ) // Enter
e.preventDefault();
});
// Create a potential postMessage connection with the parent frame.
this.parent = new api.Messenger( api.settings.parent );
// If we receive a 'back' event, we're inside an iframe.
// Send any clicks to the 'Return' link to the parent page.
this.parent.bind( 'back', function( text ) {
self.form.find('.back').text( text ).click( function( event ) {
event.preventDefault();
self.parent.send( 'close' );
});
});
// Initialize the connection with the parent frame.
this.parent.send( 'ready' );
},
loader: function() {
if ( this.loading )
return this.loading;
this.loading = $('<iframe />', {
name: this.name + '-loading-' + this.loaderUuid++
}).appendTo( this.container );
this.loading = $('<iframe />').appendTo( this.container );
return this.loading;
},
loaded: function() {
this.iframe.remove();
if ( this.iframe )
this.iframe.remove();
this.iframe = this.loading;
delete this.loading;
this.iframe.prop( 'name', this.name );
this.targetWindow( this.iframe[0].contentWindow );
},
query: function() {},
refresh: function() {
this.loader().one( 'load', this.loaded );
var self = this;
this.submit({
target: this.loader().prop('name'),
action: this.url()
if ( this.request )
this.request.abort();
this.request = $.post( this.url(), this.query() || {}, function( response ) {
var iframe = self.loader()[0].contentWindow;
self.loader().one( 'load', self.loaded );
iframe.document.open();
iframe.document.write( response );
iframe.document.close();
});
},
submit: function( props ) {
if ( props )
this.form.prop( props );
this.form.submit();
if ( props )
this.form.prop( this._formOriginalProps );
}
});
@ -415,11 +371,42 @@
// Initialize Previewer
var body = $( document.body ),
previewer = new api.Previewer({
iframe: '#customize-preview iframe',
form: '#customize-controls',
url: api.settings.preview
});
query, previewer, parent;
// Prevent the form from saving when enter is pressed.
$('#customize-controls').on( 'keydown', function( e ) {
if ( 13 === e.which ) // Enter
e.preventDefault();
});
previewer = new api.Previewer({
container: '#customize-preview',
form: '#customize-controls',
url: api.settings.preview
}, {
query: function() {
return {
customize: 'on',
theme: api.settings.theme,
customized: JSON.stringify( api.get() )
};
},
nonce: $('#_wpnonce').val(),
save: function() {
var query = $.extend( this.query(), {
action: 'customize_save',
nonce: this.nonce
}),
request = $.post( api.settings.ajax, query );
body.addClass('saving');
request.always( function() {
body.removeClass('saving');
});
}
});
$.each( api.settings.settings, function( id, data ) {
api.set( id, id, data.value, {
@ -438,6 +425,9 @@
} ) );
});
// Load the preview frame.
previewer.refresh();
// Temporary accordion code.
$('.customize-section-title').click( function() {
$( this ).parents('.customize-section').toggleClass( 'open' );
@ -446,7 +436,7 @@
// Button bindings.
$('#save').click( function( event ) {
previewer.submit();
previewer.save();
event.preventDefault();
});
@ -455,6 +445,21 @@
event.preventDefault();
});
// Create a potential postMessage connection with the parent frame.
parent = new api.Messenger( api.settings.parent );
// If we receive a 'back' event, we're inside an iframe.
// Send any clicks to the 'Return' link to the parent page.
parent.bind( 'back', function( text ) {
$('.back').text( text ).click( function( event ) {
event.preventDefault();
parent.send( 'close' );
});
});
// Initialize the connection with the parent frame.
parent.send( 'ready' );
// Control visibility for default controls
$.each({
'background_image': {