WordPress.org

Make WordPress Core

Ticket #27491: 27491.diff

File 27491.diff, 12.7 KB (added by westonruter, 5 years ago)

Re-work how and when widget forms get updated. Add widget-form-update event for widget form updates in both customizer and widgets admin page (fixes #19675).

  • src/wp-admin/css/customize-widgets.css

    diff --git src/wp-admin/css/customize-widgets.css src/wp-admin/css/customize-widgets.css
    index f8530a1..9d3fdf3 100644
     
    3737.customize-control-widget_form.previewer-loading .spinner {
    3838        opacity: 1.0;
    3939}
     40.customize-control-widget_form.widget-form-disabled .widget-content {
     41        opacity: 0.7;
     42        pointer-events: none;
     43        -moz-user-select: none;
     44        -webkit-user-select: none;
     45        -ms-user-select: none;
     46        user-select: none;
     47}
    4048
    4149.customize-control-widget_form .widget {
    4250        margin-bottom: 0;
  • src/wp-admin/js/customize-widgets.js

    diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js
    index 43233a7..d1a2d57 100644
    var WidgetCustomizer = ( function ($) { 
    88                Sidebar,
    99                SidebarCollection,
    1010                OldPreviewer,
     11                builtin_form_update_handlers,
    1112                customize = wp.customize, self = {
    1213                update_widget_ajax_action: null,
    1314                update_widget_nonce_value: null,
    var WidgetCustomizer = ( function ($) { 
    136137        self.registered_sidebars = new SidebarCollection( self.registered_sidebars );
    137138
    138139        /**
     140         * Handlers for the widget-form-update event, organized by widget ID base.
     141         * Other widgets may provide their own update handlers by adding
     142         * listeners for the widget-form-update event.
     143         */
     144        builtin_form_update_handlers = {
     145
     146                /**
     147                 * @param {jQuery.Event} e
     148                 * @param {String} args.widget_id
     149                 * @param {String} args.widget_id_base
     150                 * @param {String} args.new_form
     151                 * @param {Boolean} args.hard
     152                 * @param {wp.customize.controlConstructor.widget_form} args.customize_control
     153                 */
     154                rss: function ( e, args ) {
     155                        var old_widget_error = args.customize_control.container.find( '.widget-error:first' ),
     156                                new_widget_error = $( '<div>' + args.new_form + '</div>' ).find( '.widget-error:first' );
     157
     158                        if ( old_widget_error.length && new_widget_error.length ) {
     159                                old_widget_error.replaceWith( new_widget_error );
     160                        } else if ( old_widget_error.length ) {
     161                                old_widget_error.remove();
     162                        } else if ( new_widget_error.length ) {
     163                                args.customize_control.container.find( '.widget-content:first' ).prepend( new_widget_error );
     164                        }
     165                }
     166        };
     167
     168        /**
    139169         * On DOM ready, initialize some meta functionality independent of specific
    140170         * customizer controls.
    141171         */
    var WidgetCustomizer = ( function ($) { 
    629659                        control._setupHighlightEffects();
    630660                        control._setupUpdateUI();
    631661                        control._setupRemoveUI();
    632                         control.hook( 'init' );
    633                 },
    634 
    635                 /**
    636                  * Hooks for widgets to support living in the customizer control
    637                  */
    638                 hooks: {
    639                         _default: {},
    640                         rss: {
    641                                 formUpdated: function ( serialized_form ) {
    642                                         var control = this,
    643                                                 old_widget_error = control.container.find( '.widget-error:first' ),
    644                                                 new_widget_error = serialized_form.find( '.widget-error:first' );
    645 
    646                                         if ( old_widget_error.length && new_widget_error.length ) {
    647                                                 old_widget_error.replaceWith( new_widget_error );
    648                                         } else if ( old_widget_error.length ) {
    649                                                 old_widget_error.remove();
    650                                         } else if ( new_widget_error.length ) {
    651                                                 control.container.find( '.widget-content' ).prepend( new_widget_error );
    652                                         }
    653                                 }
    654                         }
    655                 },
    656 
    657                 /**
    658                  * Trigger an 'action' which a specific widget type can handle
    659                  *
    660                  * @param name
    661                  */
    662                 hook: function ( name ) {
    663                         var args = Array.prototype.slice.call( arguments, 1 ), handler;
    664 
    665                         if ( this.hooks[this.params.widget_id_base] && this.hooks[this.params.widget_id_base][name] ) {
    666                                 handler = this.hooks[this.params.widget_id_base][name];
    667                         } else if ( this.hooks._default[name] ) {
    668                                 handler = this.hooks._default[name];
    669                         }
    670                         if ( handler ) {
    671                                 handler.apply( this, args );
    672                         }
    673662                },
    674663
    675664                /**
    var WidgetCustomizer = ( function ($) { 
    687676
    688677                        control._update_count = 0;
    689678                        control.is_widget_updating = false;
     679                        control.live_update_mode = true;
    690680
    691681                        // Update widget whenever model changes
    692682                        control.setting.bind( function( to, from ) {
    var WidgetCustomizer = ( function ($) { 
    967957                        var control = this,
    968958                                widget_content,
    969959                                save_btn,
    970                                 update_widget_debounced;
     960                                update_widget_debounced,
     961                                form_update_event_handler;
    971962
    972963                        widget_content = control.container.find( '.widget-content' );
    973964
    var WidgetCustomizer = ( function ($) { 
    978969                        save_btn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
    979970                        save_btn.on( 'click', function ( e ) {
    980971                                e.preventDefault();
    981                                 control.updateWidget();
     972                                control.updateWidget( { disable_form: true } );
    982973                        } );
    983974
    984975                        update_widget_debounced = _.debounce( function () {
    var WidgetCustomizer = ( function ($) { 
    996987
    997988                        // Handle widgets that support live previews
    998989                        widget_content.on( 'change input propertychange', ':input', function ( e ) {
    999                                 if ( e.type === 'change' ) {
    1000                                         control.updateWidget();
    1001                                 } else if ( this.checkValidity && this.checkValidity() ) {
    1002                                         update_widget_debounced();
     990                                if ( control.live_update_mode ) {
     991                                        if ( e.type === 'change' ) {
     992                                                control.updateWidget();
     993                                        } else if ( this.checkValidity && this.checkValidity() ) {
     994                                                update_widget_debounced();
     995                                        }
    1003996                                }
    1004997                        } );
    1005998
    var WidgetCustomizer = ( function ($) { 
    10181011                                var is_rendered = !! rendered_widgets[control.params.widget_id];
    10191012                                control.container.toggleClass( 'widget-rendered', is_rendered );
    10201013                        } );
     1014
     1015                        form_update_event_handler = builtin_form_update_handlers[ control.params.widget_id_base ];
     1016                        if ( form_update_event_handler ) {
     1017                                control.container.find( '.widget:first' ).on( 'widget-form-update', form_update_event_handler );
     1018                        }
    10211019                },
    10221020
    10231021                /**
    var WidgetCustomizer = ( function ($) { 
    11471145                        var control = this,
    11481146                                instance_override,
    11491147                                complete_callback,
     1148                                widget_root,
    11501149                                update_number,
    11511150                                widget_content,
    1152                                 element_id_to_refocus = null,
    1153                                 active_input_selection_start = null,
    1154                                 active_input_selection_end = null,
    11551151                                params,
    11561152                                data,
    11571153                                inputs,
    var WidgetCustomizer = ( function ($) { 
    11701166                        control._update_count += 1;
    11711167                        update_number = control._update_count;
    11721168
    1173                         widget_content = control.container.find( '.widget-content' );
    1174 
    1175                         // @todo Support more selectors than IDs?
    1176                         if ( $.contains( control.container[0], document.activeElement ) && $( document.activeElement ).is( '[id]' ) ) {
    1177                                 element_id_to_refocus = $( document.activeElement ).prop( 'id' );
    1178                                 // @todo IE8 support: http://stackoverflow.com/a/4207763/93579
    1179                                 try {
    1180                                         active_input_selection_start = document.activeElement.selectionStart;
    1181                                         active_input_selection_end = document.activeElement.selectionEnd;
    1182                                 }
    1183                                 catch( e ) {} // catch InvalidStateError in case of checkboxes
    1184                         }
     1169                        widget_root = control.container.find( '.widget:first' );
     1170                        widget_content = control.container.find( '.widget-content:first' );
    11851171
    11861172                        control.container.addClass( 'widget-form-loading' );
    11871173                        control.container.addClass( 'previewer-loading' );
    11881174                        processing = wp.customize.state( 'processing' );
    11891175                        processing( processing() + 1 );
    11901176
     1177                        if ( ! control.live_update_mode ) {
     1178                                control.container.addClass( 'widget-form-disabled' );
     1179                        }
     1180
    11911181                        params = {};
    11921182                        params.action = self.update_widget_ajax_action;
    11931183                        params.wp_customize = 'on';
    var WidgetCustomizer = ( function ($) { 
    12171207                                        sanitized_form,
    12181208                                        sanitized_inputs,
    12191209                                        has_same_inputs_in_response,
    1220                                         is_instance_identical;
     1210                                        no_setting_change,
     1211                                        is_live_update_aborted = false,
     1212                                        event_data;
    12211213
    12221214                                if ( r.success ) {
    12231215                                        sanitized_form = $( '<div>' + r.data.form + '</div>' );
    1224 
    1225                                         control.hook( 'formUpdate', sanitized_form );
    1226 
    12271216                                        sanitized_inputs = sanitized_form.find( ':input, option' );
    12281217                                        has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
    12291218
    1230                                         if ( has_same_inputs_in_response ) {
     1219                                        if ( has_same_inputs_in_response && control.live_update_mode ) {
    12311220                                                inputs.each( function ( i ) {
    12321221                                                        var input = $( this ),
    12331222                                                                sanitized_input = $( sanitized_inputs[i] ),
    12341223                                                                property = control._getInputStatePropertyName( this ),
    1235                                                                 state,
     1224                                                                submitted_state,
    12361225                                                                sanitized_state;
    12371226
    1238                                                         state = input.data( 'state' + update_number );
     1227                                                        submitted_state = input.data( 'state' + update_number );
    12391228                                                        sanitized_state = sanitized_input.prop( property );
    12401229                                                        input.data( 'sanitized', sanitized_state );
     1230                                                        event_data = {
     1231                                                                'sanitized_state': sanitized_state,
     1232                                                                'submitted_state': submitted_state,
     1233                                                                'customize_control': control
     1234                                                        };
    12411235
    1242                                                         if ( state !== sanitized_state ) {
     1236                                                        if ( submitted_state !== sanitized_state ) {
    12431237
    12441238                                                                // Only update now if not currently focused on it,
    12451239                                                                // so that we don't cause the cursor
    12461240                                                                // it will be updated upon the change event
    12471241                                                                if ( args.ignore_active_element || ! input.is( document.activeElement ) ) {
    12481242                                                                        input.prop( property, sanitized_state );
     1243                                                                        input.trigger( 'widget-sanitary-field', [ event_data ] );
     1244                                                                } else {
     1245                                                                        input.trigger( 'widget-unsanitary-field', [ event_data ] );
    12491246                                                                }
    1250                                                                 control.hook( 'unsanitaryField', input, sanitized_state, state );
    12511247
    12521248                                                        } else {
    1253                                                                 control.hook( 'sanitaryField', input, state );
     1249                                                                input.trigger( 'widget-sanitary-field', [ event_data ] );
    12541250                                                        }
    12551251                                                } );
    1256                                                 control.hook( 'formUpdated', sanitized_form );
     1252
     1253                                        } else if ( control.live_update_mode ) {
     1254                                                control.live_update_mode = false;
     1255                                                control.container.find( 'input[name="savewidget"]' ).show();
     1256                                                is_live_update_aborted = true;
    12571257                                        } else {
    1258                                                 widget_content.html( sanitized_form.html() );
    1259                                                 if ( element_id_to_refocus ) {
    1260                                                         // not using jQuery selector so we don't have to worry about escaping IDs with brackets and other characters
    1261                                                         $( document.getElementById( element_id_to_refocus ) )
    1262                                                                 .prop( {
    1263                                                                         selectionStart: active_input_selection_start,
    1264                                                                         selectionEnd: active_input_selection_end
    1265                                                                 } )
    1266                                                                 .focus();
    1267                                                 }
    1268                                                 control.hook( 'formRefreshed' );
     1258                                                widget_content.html( r.data.form );
     1259                                                control.container.removeClass( 'widget-form-disabled' );
     1260                                        }
     1261
     1262                                        if ( ! is_live_update_aborted ) {
     1263                                                event_data = {
     1264                                                        'widget_id': control.params.widget_id,
     1265                                                        'widget_id_base': control.params.widget_id_base,
     1266                                                        'new_form': r.data.form,
     1267                                                        'hard': ! control.live_update_mode, // dynamic fields may need to be re-initialized (e.g. Chosen)
     1268                                                        'customize_control': control
     1269                                                };
     1270                                                widget_root.trigger( 'widget-form-update', [ event_data ] ); // @todo THIS IS NOT GETTING LISTENED TO
    12691271                                        }
    12701272
    12711273                                        /**
    var WidgetCustomizer = ( function ($) { 
    12731275                                         * needing to be rendered, and so we can preempt the event for the
    12741276                                         * preview finishing loading.
    12751277                                         */
    1276                                         is_instance_identical = _( control.setting() ).isEqual( r.data.instance );
    1277                                         if ( is_instance_identical ) {
     1278                                        no_setting_change = is_live_update_aborted || _( control.setting() ).isEqual( r.data.instance );
     1279                                        if ( no_setting_change ) {
    12781280                                                control.container.removeClass( 'previewer-loading' );
    12791281                                        } else {
    12801282                                                control.is_widget_updating = true; // suppress triggering another updateWidget
    var WidgetCustomizer = ( function ($) { 
    12831285                                        }
    12841286
    12851287                                        if ( complete_callback ) {
    1286                                                 complete_callback.call( control, null, { no_change: is_instance_identical, ajax_finished: true } );
     1288                                                complete_callback.call( control, null, { no_change: no_setting_change, ajax_finished: true } );
    12871289                                        }
    12881290                                } else {
    12891291                                        window.console && window.console.log( r );
  • src/wp-admin/js/widgets.js

    diff --git src/wp-admin/js/widgets.js src/wp-admin/js/widgets.js
    index 046907b..9f935ed 100644
    wpWidgets = { 
    332332
    333333        save : function( widget, del, animate, order ) {
    334334                var sidebarId = widget.closest('div.widgets-sortables').attr('id'),
    335                         data = widget.find('form').serialize(), a;
     335                        data = widget.find('form').serialize(), a, eventData;
    336336
    337337                widget = $(widget);
    338338                $('.spinner', widget).show();
    wpWidgets = { 
    374374                        } else {
    375375                                $('.spinner').hide();
    376376                                if ( r && r.length > 2 ) {
    377                                         $( 'div.widget-content', widget ).html(r);
     377                                        $( 'div.widget-content', widget ).html( r );
    378378                                        wpWidgets.appendTitle( widget );
     379                                        eventData = {
     380                                                'widget_id': widget.find( 'input.widget-id' ).val(),
     381                                                'widget_id_base': widget.find( 'input.id_base' ).val(),
     382                                                'new_form': r,
     383                                                'hard': true, // soft updates only in customizer
     384                                                'customize_control': null
     385                                        };
     386                                        widget.trigger( 'widget-form-update', [ eventData ] );
    379387                                }
    380388                        }
    381389                        if ( order ) {