Make WordPress Core

Changeset 27909


Ignore:
Timestamp:
04/02/2014 06:20:00 PM (13 years ago)
Author:
ocean90
Message:

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.

Location:
trunk/src/wp-admin
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/customize-widgets.css

    r27824 r27909  
    3737.customize-control-widget_form.previewer-loading .spinner {
    3838        opacity: 1.0;
     39}
     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;
    3947}
    4048
  • trunk/src/wp-admin/js/customize-widgets.js

    r27907 r27909  
    99                SidebarCollection,
    1010                OldPreviewer,
     11                builtin_form_sync_handlers,
    1112                customize = wp.customize, self = {
    1213                nonce: null,
    … …  
    132133
    133134        /**
     135         * Handlers for the widget-synced event, organized by widget ID base.
     136         * Other widgets may provide their own update handlers by adding
     137         * listeners for the widget-synced event.
     138         */
     139        builtin_form_sync_handlers = {
     140
     141                /**
     142                 * @param {jQuery.Event} e
     143                 * @param {jQuery} widget_el
     144                 * @param {String} new_form
     145                 */
     146                rss: function ( e, widget_el, new_form ) {
     147                        var old_widget_error = widget_el.find( '.widget-error:first' ),
     148                                new_widget_error = $( '<div>' + new_form + '</div>' ).find( '.widget-error:first' );
     149
     150                        if ( old_widget_error.length && new_widget_error.length ) {
     151                                old_widget_error.replaceWith( new_widget_error );
     152                        } else if ( old_widget_error.length ) {
     153                                old_widget_error.remove();
     154                        } else if ( new_widget_error.length ) {
     155                                widget_el.find( '.widget-content:first' ).prepend( new_widget_error );
     156                        }
     157                }
     158        };
     159
     160        /**
    134161         * On DOM ready, initialize some meta functionality independent of specific
    135162         * customizer controls.
    … …  
    455482                        var control = this,
    456483                                control_html,
     484                                widget_el,
    457485                                customize_control_type = 'widget_form',
    458486                                customize_control,
    … …  
    489517                                widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
    490518                        }
     519                        widget_el = $( control_html );
    491520
    492521                        customize_control = $( '<li></li>' );
    493522                        customize_control.addClass( 'customize-control' );
    494523                        customize_control.addClass( 'customize-control-' + customize_control_type );
    495                         customize_control.append( $( control_html ) );
     524                        customize_control.append( widget_el );
    496525                        customize_control.find( '> .widget-icon' ).remove();
    497526                        if ( widget.get( 'is_multi' ) ) {
    … …  
    579608                        } );
    580609
     610                        $( document ).trigger( 'widget-added', [ widget_el ] );
     611
    581612                        return widget_form_control;
    582613                }
    … …  
    603634                        control._setupUpdateUI();
    604635                        control._setupRemoveUI();
    605                         control.hook( 'init' );
    606                 },
    607 
    608                 /**
    609                  * Hooks for widgets to support living in the customizer control
    610                  */
    611                 hooks: {
    612                         _default: {},
    613                         rss: {
    614                                 formUpdated: function ( serialized_form ) {
    615                                         var control = this,
    616                                                 old_widget_error = control.container.find( '.widget-error:first' ),
    617                                                 new_widget_error = serialized_form.find( '.widget-error:first' );
    618 
    619                                         if ( old_widget_error.length && new_widget_error.length ) {
    620                                                 old_widget_error.replaceWith( new_widget_error );
    621                                         } else if ( old_widget_error.length ) {
    622                                                 old_widget_error.remove();
    623                                         } else if ( new_widget_error.length ) {
    624                                                 control.container.find( '.widget-content' ).prepend( new_widget_error );
    625                                         }
    626                                 }
    627                         }
    628                 },
    629 
    630                 /**
    631                  * Trigger an 'action' which a specific widget type can handle
    632                  *
    633                  * @param name
    634                  */
    635                 hook: function ( name ) {
    636                         var args = Array.prototype.slice.call( arguments, 1 ), handler;
    637 
    638                         if ( this.hooks[this.params.widget_id_base] && this.hooks[this.params.widget_id_base][name] ) {
    639                                 handler = this.hooks[this.params.widget_id_base][name];
    640                         } else if ( this.hooks._default[name] ) {
    641                                 handler = this.hooks._default[name];
    642                         }
    643                         if ( handler ) {
    644                                 handler.apply( this, args );
    645                         }
    646636                },
    647637
    … …  
    661651                        control._update_count = 0;
    662652                        control.is_widget_updating = false;
     653                        control.live_update_mode = true;
    663654
    664655                        // Update widget whenever model changes
    … …  
    946937                _setupUpdateUI: function () {
    947938                        var control = this,
     939                                widget_root,
    948940                                widget_content,
    949941                                save_btn,
    950                                 update_widget_debounced;
    951 
    952                         widget_content = control.container.find( '.widget-content' );
     942                                update_widget_debounced,
     943                                form_update_event_handler;
     944
     945                        widget_root = control.container.find( '.widget:first' );
     946                        widget_content = widget_root.find( '.widget-content:first' );
    953947
    954948                        // Configure update button
    … …  
    959953                        save_btn.on( 'click', function ( e ) {
    960954                                e.preventDefault();
    961                                 control.updateWidget();
     955                                control.updateWidget( { disable_form: true } );
    962956                        } );
    963957
    … …  
    977971                        // Handle widgets that support live previews
    978972                        widget_content.on( 'change input propertychange', ':input', function ( e ) {
    979                                 if ( e.type === 'change' ) {
    980                                         control.updateWidget();
    981                                 } else if ( this.checkValidity && this.checkValidity() ) {
    982                                         update_widget_debounced();
     973                                if ( control.live_update_mode ) {
     974                                        if ( e.type === 'change' ) {
     975                                                control.updateWidget();
     976                                        } else if ( this.checkValidity && this.checkValidity() ) {
     977                                                update_widget_debounced();
     978                                        }
    983979                                }
    984980                        } );
    … …  
    999995                                control.container.toggleClass( 'widget-rendered', is_rendered );
    1000996                        } );
     997
     998                        form_update_event_handler = builtin_form_sync_handlers[ control.params.widget_id_base ];
     999                        if ( form_update_event_handler ) {
     1000                                $( document ).on( 'widget-synced', function ( e, widget_el ) {
     1001                                        if ( widget_root.is( widget_el ) ) {
     1002                                                form_update_event_handler.apply( document, arguments );
     1003                                        }
     1004                                } );
     1005                        }
    10011006                },
    10021007
    … …  
    10561061
    10571062                /**
     1063                 * Find all inputs in a widget container that should be considered when
     1064                 * comparing the loaded form with the sanitized form, whose fields will
     1065                 * be aligned to copy the sanitized over. The elements returned by this
     1066                 * are passed into this._getInputsSignature(), and they are iterated
     1067                 * over when copying sanitized values over to the the form loaded.
     1068                 *
     1069                 * @param {jQuery} container element in which to look for inputs
     1070                 * @returns {jQuery} inputs
     1071                 * @private
     1072                 */
     1073                _getInputs: function ( container ) {
     1074                        return $( container ).find( ':input[name]' );
     1075                },
     1076
     1077                /**
    10581078                 * Iterate over supplied inputs and create a signature string for all of them together.
    10591079                 * This string can be used to compare whether or not the form has all of the same fields.
    … …  
    10671087                                input = $( input );
    10681088                                var signature_parts;
    1069                                 if ( input.is( 'option' ) ) {
    1070                                         signature_parts = [ input.prop( 'nodeName' ), input.prop( 'value' ) ];
    1071                                 } else if ( input.is( ':checkbox, :radio' ) ) {
    1072                                         signature_parts = [ input.prop( 'type' ), input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
     1089                                if ( input.is( ':checkbox, :radio' ) ) {
     1090                                        signature_parts = [ input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
    10731091                                } else {
    1074                                         signature_parts = [ input.prop( 'nodeName' ), input.attr( 'id' ), input.attr( 'name' ), input.attr( 'type' ) ];
     1092                                        signature_parts = [ input.attr( 'id' ), input.attr( 'name' ) ];
    10751093                                }
    10761094                                return signature_parts.join( ',' );
    … …  
    10901108                        if ( input.is( ':radio, :checkbox' ) ) {
    10911109                                return 'checked';
    1092                         } else if ( input.is( 'option' ) ) {
    1093                                 return 'selected';
    10941110                        } else {
    10951111                                return 'value';
    … …  
    11281144                                instance_override,
    11291145                                complete_callback,
     1146                                widget_root,
    11301147                                update_number,
    11311148                                widget_content,
    1132                                 element_id_to_refocus = null,
    1133                                 active_input_selection_start = null,
    1134                                 active_input_selection_end = null,
    11351149                                params,
    11361150                                data,
    11371151                                inputs,
    11381152                                processing,
    1139                                 jqxhr;
     1153                                jqxhr,
     1154                                is_changed;
    11401155
    11411156                        args = $.extend( {
    … …  
    11511166                        update_number = control._update_count;
    11521167
    1153                         widget_content = control.container.find( '.widget-content' );
     1168                        widget_root = control.container.find( '.widget:first' );
     1169                        widget_content = widget_root.find( '.widget-content:first' );
    11541170
    11551171                        // Remove a previous error message
    11561172                        widget_content.find( '.widget-error' ).remove();
    1157 
    1158                         // @todo Support more selectors than IDs?
    1159                         if ( $.contains( control.container[0], document.activeElement ) && $( document.activeElement ).is( '[id]' ) ) {
    1160                                 element_id_to_refocus = $( document.activeElement ).prop( 'id' );
    1161                                 // @todo IE8 support: http://stackoverflow.com/a/4207763/93579
    1162                                 try {
    1163                                         active_input_selection_start = document.activeElement.selectionStart;
    1164                                         active_input_selection_end = document.activeElement.selectionEnd;
    1165                                 }
    1166                                 catch( e ) {} // catch InvalidStateError in case of checkboxes
    1167                         }
    11681173
    11691174                        control.container.addClass( 'widget-form-loading' );
    … …  
    11721177                        processing( processing() + 1 );
    11731178
     1179                        if ( ! control.live_update_mode ) {
     1180                                control.container.addClass( 'widget-form-disabled' );
     1181                        }
     1182
    11741183                        params = {};
    11751184                        params.action = 'update-widget';
    … …  
    11781187
    11791188                        data = $.param( params );
    1180                         inputs = widget_content.find( ':input, option' );
     1189                        inputs = control._getInputs( widget_content );
    11811190
    11821191                        // Store the value we're submitting in data so that when the response comes back,
    … …  
    12011210                                        sanitized_inputs,
    12021211                                        has_same_inputs_in_response,
    1203                                         is_instance_identical;
     1212                                        is_live_update_aborted = false;
    12041213
    12051214                                // Check if the user is logged out.
    … …  
    12211230                                if ( r.success ) {
    12221231                                        sanitized_form = $( '<div>' + r.data.form + '</div>' );
    1223 
    1224                                         control.hook( 'formUpdate', sanitized_form );
    1225 
    1226                                         sanitized_inputs = sanitized_form.find( ':input, option' );
     1232                                        sanitized_inputs = control._getInputs( sanitized_form );
    12271233                                        has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
    12281234
    1229                                         if ( has_same_inputs_in_response ) {
     1235                                        // Restore live update mode if sanitized fields are now aligned with the existing fields
     1236                                        if ( has_same_inputs_in_response && ! control.live_update_mode ) {
     1237                                                control.live_update_mode = true;
     1238                                                control.container.removeClass( 'widget-form-disabled' );
     1239                                                control.container.find( 'input[name="savewidget"]' ).hide();
     1240                                        }
     1241
     1242                                        // Sync sanitized field states to existing fields if they are aligned
     1243                                        if ( has_same_inputs_in_response && control.live_update_mode ) {
    12301244                                                inputs.each( function ( i ) {
    12311245                                                        var input = $( this ),
    12321246                                                                sanitized_input = $( sanitized_inputs[i] ),
    12331247                                                                property = control._getInputStatePropertyName( this ),
    1234                                                                 state,
    1235                                                                 sanitized_state;
    1236 
    1237                                                         state = input.data( 'state' + update_number );
     1248                                                                submitted_state,
     1249                                                                sanitized_state,
     1250                                                                can_update_state;
     1251
     1252                                                        submitted_state = input.data( 'state' + update_number );
    12381253                                                        sanitized_state = sanitized_input.prop( property );
    12391254                                                        input.data( 'sanitized', sanitized_state );
    12401255
    1241                                                         if ( state !== sanitized_state ) {
    1242 
    1243                                                                 // Only update now if not currently focused on it,
    1244                                                                 // so that we don't cause the cursor
    1245                                                                 // it will be updated upon the change event
    1246                                                                 if ( args.ignore_active_element || ! input.is( document.activeElement ) ) {
    1247                                                                         input.prop( property, sanitized_state );
    1248                                                                 }
    1249                                                                 control.hook( 'unsanitaryField', input, sanitized_state, state );
    1250 
    1251                                                         } else {
    1252                                                                 control.hook( 'sanitaryField', input, state );
     1256                                                        can_update_state = (
     1257                                                                submitted_state !== sanitized_state &&
     1258                                                                ( args.ignore_active_element || ! input.is( document.activeElement ) )
     1259                                                        );
     1260                                                        if ( can_update_state ) {
     1261                                                                input.prop( property, sanitized_state );
    12531262                                                        }
    12541263                                                } );
    1255                                                 control.hook( 'formUpdated', sanitized_form );
     1264                                                $( document ).trigger( 'widget-synced', [ widget_root, r.data.form ] );
     1265
     1266                                        // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled
     1267                                        } else if ( control.live_update_mode ) {
     1268                                                control.live_update_mode = false;
     1269                                                control.container.find( 'input[name="savewidget"]' ).show();
     1270                                                is_live_update_aborted = true;
     1271                                        // Otherwise, replace existing form with the sanitized form
    12561272                                        } else {
    1257                                                 widget_content.html( sanitized_form.html() );
    1258                                                 if ( element_id_to_refocus ) {
    1259                                                         // not using jQuery selector so we don't have to worry about escaping IDs with brackets and other characters
    1260                                                         $( document.getElementById( element_id_to_refocus ) )
    1261                                                                 .prop( {
    1262                                                                         selectionStart: active_input_selection_start,
    1263                                                                         selectionEnd: active_input_selection_end
    1264                                                                 } )
    1265                                                                 .focus();
    1266                                                 }
    1267                                                 control.hook( 'formRefreshed' );
     1273                                                widget_content.html( r.data.form );
     1274                                                control.container.removeClass( 'widget-form-disabled' );
     1275                                                $( document ).trigger( 'widget-updated', [ widget_root ] );
    12681276                                        }
    12691277
    … …  
    12731281                                         * preview finishing loading.
    12741282                                         */
    1275                                         is_instance_identical = _( control.setting() ).isEqual( r.data.instance );
    1276                                         if ( ! is_instance_identical ) {
     1283                                        is_changed = ! is_live_update_aborted && ! _( control.setting() ).isEqual( r.data.instance );
     1284                                        if ( is_changed ) {
    12771285                                                control.is_widget_updating = true; // suppress triggering another updateWidget
    12781286                                                control.setting( r.data.instance );
    … …  
    12811289
    12821290                                        if ( complete_callback ) {
    1283                                                 complete_callback.call( control, null, { no_change: is_instance_identical, ajax_finished: true } );
     1291                                                complete_callback.call( control, null, { no_change: ! is_changed, ajax_finished: true } );
    12841292                                        }
    12851293                                } else {
Note: See TracChangeset for help on using the changeset viewer.