Widget Customizer: Improve support for dynamically-created inputs.

* Re-work how and when widget forms get updated.
* Replace ad hoc hooks system with jQuery events,
* Add `widget-updated`/`widget-synced` events for widget soft/hard updates.
* Enter into a non-live form update mode, where the Apply button is restored when a sanitized form does not have the same fields as currently in the form, and so the fields cannot be easily updated to their sanitized values without doing a complete form replacement. Also restores live update mode if sanitized fields are aligned with the existing fields again.

Note: jQuery events are *not* final yet, see #19675.

props westonruter.
see #27491.
Built from https://develop.svn.wordpress.org/trunk@27909


git-svn-id: http://core.svn.wordpress.org/trunk@27740 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Dominik Schilling 2014-04-02 18:20:16 +00:00
parent 92467fbd63
commit 944fba8e2f
6 changed files with 136 additions and 112 deletions

View File

@ -37,6 +37,14 @@
.customize-control-widget_form.previewer-loading .spinner {
opacity: 1.0;
}
.customize-control-widget_form.widget-form-disabled .widget-content {
opacity: 0.7;
pointer-events: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.customize-control-widget_form .widget {
margin-bottom: 0;

File diff suppressed because one or more lines are too long

View File

@ -37,6 +37,14 @@
.customize-control-widget_form.previewer-loading .spinner {
opacity: 1.0;
}
.customize-control-widget_form.widget-form-disabled .widget-content {
opacity: 0.7;
pointer-events: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.customize-control-widget_form .widget {
margin-bottom: 0;

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,7 @@ var WidgetCustomizer = ( function ($) {
Sidebar,
SidebarCollection,
OldPreviewer,
builtin_form_sync_handlers,
customize = wp.customize, self = {
nonce: null,
i18n: {
@ -130,6 +131,32 @@ var WidgetCustomizer = ( function ($) {
} );
self.registered_sidebars = new SidebarCollection( self.registered_sidebars );
/**
* Handlers for the widget-synced event, organized by widget ID base.
* Other widgets may provide their own update handlers by adding
* listeners for the widget-synced event.
*/
builtin_form_sync_handlers = {
/**
* @param {jQuery.Event} e
* @param {jQuery} widget_el
* @param {String} new_form
*/
rss: function ( e, widget_el, new_form ) {
var old_widget_error = widget_el.find( '.widget-error:first' ),
new_widget_error = $( '<div>' + new_form + '</div>' ).find( '.widget-error:first' );
if ( old_widget_error.length && new_widget_error.length ) {
old_widget_error.replaceWith( new_widget_error );
} else if ( old_widget_error.length ) {
old_widget_error.remove();
} else if ( new_widget_error.length ) {
widget_el.find( '.widget-content:first' ).prepend( new_widget_error );
}
}
};
/**
* On DOM ready, initialize some meta functionality independent of specific
* customizer controls.
@ -454,6 +481,7 @@ var WidgetCustomizer = ( function ($) {
addWidget: function ( widget_id ) {
var control = this,
control_html,
widget_el,
customize_control_type = 'widget_form',
customize_control,
parsed_widget_id = parse_widget_id( widget_id ),
@ -488,11 +516,12 @@ var WidgetCustomizer = ( function ($) {
} else {
widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
}
widget_el = $( control_html );
customize_control = $( '<li></li>' );
customize_control.addClass( 'customize-control' );
customize_control.addClass( 'customize-control-' + customize_control_type );
customize_control.append( $( control_html ) );
customize_control.append( widget_el );
customize_control.find( '> .widget-icon' ).remove();
if ( widget.get( 'is_multi' ) ) {
customize_control.find( 'input[name="widget_number"]' ).val( widget_number );
@ -578,6 +607,8 @@ var WidgetCustomizer = ( function ($) {
}
} );
$( document ).trigger( 'widget-added', [ widget_el ] );
return widget_form_control;
}
@ -602,47 +633,6 @@ var WidgetCustomizer = ( function ($) {
control._setupHighlightEffects();
control._setupUpdateUI();
control._setupRemoveUI();
control.hook( 'init' );
},
/**
* Hooks for widgets to support living in the customizer control
*/
hooks: {
_default: {},
rss: {
formUpdated: function ( serialized_form ) {
var control = this,
old_widget_error = control.container.find( '.widget-error:first' ),
new_widget_error = serialized_form.find( '.widget-error:first' );
if ( old_widget_error.length && new_widget_error.length ) {
old_widget_error.replaceWith( new_widget_error );
} else if ( old_widget_error.length ) {
old_widget_error.remove();
} else if ( new_widget_error.length ) {
control.container.find( '.widget-content' ).prepend( new_widget_error );
}
}
}
},
/**
* Trigger an 'action' which a specific widget type can handle
*
* @param name
*/
hook: function ( name ) {
var args = Array.prototype.slice.call( arguments, 1 ), handler;
if ( this.hooks[this.params.widget_id_base] && this.hooks[this.params.widget_id_base][name] ) {
handler = this.hooks[this.params.widget_id_base][name];
} else if ( this.hooks._default[name] ) {
handler = this.hooks._default[name];
}
if ( handler ) {
handler.apply( this, args );
}
},
/**
@ -660,6 +650,7 @@ var WidgetCustomizer = ( function ($) {
control._update_count = 0;
control.is_widget_updating = false;
control.live_update_mode = true;
// Update widget whenever model changes
control.setting.bind( function( to, from ) {
@ -945,11 +936,14 @@ var WidgetCustomizer = ( function ($) {
*/
_setupUpdateUI: function () {
var control = this,
widget_root,
widget_content,
save_btn,
update_widget_debounced;
update_widget_debounced,
form_update_event_handler;
widget_content = control.container.find( '.widget-content' );
widget_root = control.container.find( '.widget:first' );
widget_content = widget_root.find( '.widget-content:first' );
// Configure update button
save_btn = control.container.find( '.widget-control-save' );
@ -958,7 +952,7 @@ var WidgetCustomizer = ( function ($) {
save_btn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
save_btn.on( 'click', function ( e ) {
e.preventDefault();
control.updateWidget();
control.updateWidget( { disable_form: true } );
} );
update_widget_debounced = _.debounce( function () {
@ -976,10 +970,12 @@ var WidgetCustomizer = ( function ($) {
// Handle widgets that support live previews
widget_content.on( 'change input propertychange', ':input', function ( e ) {
if ( e.type === 'change' ) {
control.updateWidget();
} else if ( this.checkValidity && this.checkValidity() ) {
update_widget_debounced();
if ( control.live_update_mode ) {
if ( e.type === 'change' ) {
control.updateWidget();
} else if ( this.checkValidity && this.checkValidity() ) {
update_widget_debounced();
}
}
} );
@ -998,6 +994,15 @@ var WidgetCustomizer = ( function ($) {
var is_rendered = !! rendered_widgets[control.params.widget_id];
control.container.toggleClass( 'widget-rendered', is_rendered );
} );
form_update_event_handler = builtin_form_sync_handlers[ control.params.widget_id_base ];
if ( form_update_event_handler ) {
$( document ).on( 'widget-synced', function ( e, widget_el ) {
if ( widget_root.is( widget_el ) ) {
form_update_event_handler.apply( document, arguments );
}
} );
}
},
/**
@ -1054,6 +1059,21 @@ var WidgetCustomizer = ( function ($) {
}
},
/**
* Find all inputs in a widget container that should be considered when
* comparing the loaded form with the sanitized form, whose fields will
* be aligned to copy the sanitized over. The elements returned by this
* are passed into this._getInputsSignature(), and they are iterated
* over when copying sanitized values over to the the form loaded.
*
* @param {jQuery} container element in which to look for inputs
* @returns {jQuery} inputs
* @private
*/
_getInputs: function ( container ) {
return $( container ).find( ':input[name]' );
},
/**
* Iterate over supplied inputs and create a signature string for all of them together.
* This string can be used to compare whether or not the form has all of the same fields.
@ -1066,12 +1086,10 @@ var WidgetCustomizer = ( function ($) {
var inputs_signatures = _( inputs ).map( function ( input ) {
input = $( input );
var signature_parts;
if ( input.is( 'option' ) ) {
signature_parts = [ input.prop( 'nodeName' ), input.prop( 'value' ) ];
} else if ( input.is( ':checkbox, :radio' ) ) {
signature_parts = [ input.prop( 'type' ), input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
if ( input.is( ':checkbox, :radio' ) ) {
signature_parts = [ input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
} else {
signature_parts = [ input.prop( 'nodeName' ), input.attr( 'id' ), input.attr( 'name' ), input.attr( 'type' ) ];
signature_parts = [ input.attr( 'id' ), input.attr( 'name' ) ];
}
return signature_parts.join( ',' );
} );
@ -1089,8 +1107,6 @@ var WidgetCustomizer = ( function ($) {
input = $( input );
if ( input.is( ':radio, :checkbox' ) ) {
return 'checked';
} else if ( input.is( 'option' ) ) {
return 'selected';
} else {
return 'value';
}
@ -1127,16 +1143,15 @@ var WidgetCustomizer = ( function ($) {
var control = this,
instance_override,
complete_callback,
widget_root,
update_number,
widget_content,
element_id_to_refocus = null,
active_input_selection_start = null,
active_input_selection_end = null,
params,
data,
inputs,
processing,
jqxhr;
jqxhr,
is_changed;
args = $.extend( {
instance: null,
@ -1150,34 +1165,28 @@ var WidgetCustomizer = ( function ($) {
control._update_count += 1;
update_number = control._update_count;
widget_content = control.container.find( '.widget-content' );
widget_root = control.container.find( '.widget:first' );
widget_content = widget_root.find( '.widget-content:first' );
// Remove a previous error message
widget_content.find( '.widget-error' ).remove();
// @todo Support more selectors than IDs?
if ( $.contains( control.container[0], document.activeElement ) && $( document.activeElement ).is( '[id]' ) ) {
element_id_to_refocus = $( document.activeElement ).prop( 'id' );
// @todo IE8 support: http://stackoverflow.com/a/4207763/93579
try {
active_input_selection_start = document.activeElement.selectionStart;
active_input_selection_end = document.activeElement.selectionEnd;
}
catch( e ) {} // catch InvalidStateError in case of checkboxes
}
control.container.addClass( 'widget-form-loading' );
control.container.addClass( 'previewer-loading' );
processing = wp.customize.state( 'processing' );
processing( processing() + 1 );
if ( ! control.live_update_mode ) {
control.container.addClass( 'widget-form-disabled' );
}
params = {};
params.action = 'update-widget';
params.wp_customize = 'on';
params.nonce = self.nonce;
data = $.param( params );
inputs = widget_content.find( ':input, option' );
inputs = control._getInputs( widget_content );
// Store the value we're submitting in data so that when the response comes back,
// we know if it got sanitized; if there is no difference in the sanitized value,
@ -1200,7 +1209,7 @@ var WidgetCustomizer = ( function ($) {
sanitized_form,
sanitized_inputs,
has_same_inputs_in_response,
is_instance_identical;
is_live_update_aborted = false;
// Check if the user is logged out.
if ( '0' === r ) {
@ -1220,51 +1229,50 @@ var WidgetCustomizer = ( function ($) {
if ( r.success ) {
sanitized_form = $( '<div>' + r.data.form + '</div>' );
control.hook( 'formUpdate', sanitized_form );
sanitized_inputs = sanitized_form.find( ':input, option' );
sanitized_inputs = control._getInputs( sanitized_form );
has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
if ( has_same_inputs_in_response ) {
// Restore live update mode if sanitized fields are now aligned with the existing fields
if ( has_same_inputs_in_response && ! control.live_update_mode ) {
control.live_update_mode = true;
control.container.removeClass( 'widget-form-disabled' );
control.container.find( 'input[name="savewidget"]' ).hide();
}
// Sync sanitized field states to existing fields if they are aligned
if ( has_same_inputs_in_response && control.live_update_mode ) {
inputs.each( function ( i ) {
var input = $( this ),
sanitized_input = $( sanitized_inputs[i] ),
property = control._getInputStatePropertyName( this ),
state,
sanitized_state;
submitted_state,
sanitized_state,
can_update_state;
state = input.data( 'state' + update_number );
submitted_state = input.data( 'state' + update_number );
sanitized_state = sanitized_input.prop( property );
input.data( 'sanitized', sanitized_state );
if ( state !== sanitized_state ) {
// Only update now if not currently focused on it,
// so that we don't cause the cursor
// it will be updated upon the change event
if ( args.ignore_active_element || ! input.is( document.activeElement ) ) {
input.prop( property, sanitized_state );
}
control.hook( 'unsanitaryField', input, sanitized_state, state );
} else {
control.hook( 'sanitaryField', input, state );
can_update_state = (
submitted_state !== sanitized_state &&
( args.ignore_active_element || ! input.is( document.activeElement ) )
);
if ( can_update_state ) {
input.prop( property, sanitized_state );
}
} );
control.hook( 'formUpdated', sanitized_form );
$( document ).trigger( 'widget-synced', [ widget_root, r.data.form ] );
// Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled
} else if ( control.live_update_mode ) {
control.live_update_mode = false;
control.container.find( 'input[name="savewidget"]' ).show();
is_live_update_aborted = true;
// Otherwise, replace existing form with the sanitized form
} else {
widget_content.html( sanitized_form.html() );
if ( element_id_to_refocus ) {
// not using jQuery selector so we don't have to worry about escaping IDs with brackets and other characters
$( document.getElementById( element_id_to_refocus ) )
.prop( {
selectionStart: active_input_selection_start,
selectionEnd: active_input_selection_end
} )
.focus();
}
control.hook( 'formRefreshed' );
widget_content.html( r.data.form );
control.container.removeClass( 'widget-form-disabled' );
$( document ).trigger( 'widget-updated', [ widget_root ] );
}
/**
@ -1272,15 +1280,15 @@ var WidgetCustomizer = ( function ($) {
* needing to be rendered, and so we can preempt the event for the
* preview finishing loading.
*/
is_instance_identical = _( control.setting() ).isEqual( r.data.instance );
if ( ! is_instance_identical ) {
is_changed = ! is_live_update_aborted && ! _( control.setting() ).isEqual( r.data.instance );
if ( is_changed ) {
control.is_widget_updating = true; // suppress triggering another updateWidget
control.setting( r.data.instance );
control.is_widget_updating = false;
}
if ( complete_callback ) {
complete_callback.call( control, null, { no_change: is_instance_identical, ajax_finished: true } );
complete_callback.call( control, null, { no_change: ! is_changed, ajax_finished: true } );
}
} else {
message = self.i18n.error;

File diff suppressed because one or more lines are too long