Make WordPress Core

Changeset 27909


Ignore:
Timestamp:
04/02/2014 06:20:00 PM (11 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.