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:
parent
92467fbd63
commit
944fba8e2f
|
@ -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
|
@ -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
|
@ -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
Loading…
Reference in New Issue