diff --git a/src/wp-admin/css/customize-widgets.css b/src/wp-admin/css/customize-widgets.css index 1142a47b05..a7030592d9 100644 --- a/src/wp-admin/css/customize-widgets.css +++ b/src/wp-admin/css/customize-widgets.css @@ -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; diff --git a/src/wp-admin/js/customize-widgets.js b/src/wp-admin/js/customize-widgets.js index b28bba85c3..480806a9c7 100644 --- a/src/wp-admin/js/customize-widgets.js +++ b/src/wp-admin/js/customize-widgets.js @@ -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 = $( '
' + new_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 ) { + 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 = $( '
  • ' ); 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 = $( '
    ' + r.data.form + '
    ' ); - - 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;