Make WordPress Core

Changeset 41352


Ignore:
Timestamp:
09/08/2017 07:10:59 PM (9 years ago)
Author:
westonruter
Message:

Widgets: Add dirty state tracking for widgets on admin screen.

  • Mark a widget as dirty when a field input triggers a change or input event; clear dirty state when widget is successfully saved.
  • Disable Save button and re-label "Saved" when widget not dirty.
  • Show AYS dialog when leaving widgets admin screen with unsaved changes.
  • When widgets are dirty, expand all unsaved widgets at AYS check and focus on first one.
  • Change "Close" link to "Done"; hide link when widget is dirty and reveal when saved.
  • The "Done" link persistently appears in the Customizer even after making a change (when the widget is dirty) because changes are autosaved into the changeset.
  • Prevent saving widget when form fails checkValidity.
  • Fix frequency of triggering of change event on the rich Text widget's textarea limited now to when there are actual changes.
  • Add a class of widget-dirty to widget containers when the widget has unsaved changes.

Props westonruter, timmydcrawford, melchoyce.
Fixes #41610, #23120.

Location:
trunk/src
Files:
5 edited

Legend:

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

    r41339 r41352  
    4141        padding: 1px 15px 15px 15px;
    4242        line-height: 16px;
     43}
     44
     45.widget.widget-dirty .widget-control-close-wrapper {
     46        display: none;
    4347}
    4448
  • trunk/src/wp-admin/includes/widgets.php

    r40480 r41352  
    254254        <div class="widget-control-actions">
    255255                <div class="alignleft">
    256                         <button type="button" class="button-link button-link-delete widget-control-remove"><?php _e( 'Delete' ); ?></button> |
    257                         <button type="button" class="button-link widget-control-close"><?php _e( 'Close' ); ?></button>
     256                        <button type="button" class="button-link button-link-delete widget-control-remove"><?php _e( 'Delete' ); ?></button>
     257                        <span class="widget-control-close-wrapper">
     258                                |
     259                                <button type="button" class="button-link widget-control-close"><?php _e( 'Done' ); ?></button>
     260                        </span>
    258261                </div>
    259262                <div class="alignright<?php if ( 'noform' === $has_form ) echo ' widget-control-noform'; ?>">
  • trunk/src/wp-admin/js/widgets.js

    r40480 r41352  
    88         * A closed Sidebar that gets a Widget dragged over it.
    99         *
    10          * @var element|null
     10         * @var {element|null}
    1111         */
    1212        hoveredSidebar: null,
     13
     14        /**
     15         * Translations.
     16         *
     17         * Exported from PHP in wp_default_scripts().
     18         *
     19         * @var {object}
     20         */
     21        l10n: {
     22                save: '{save}',
     23                saved: '{saved}',
     24                saveAlert: '{saveAlert}'
     25        },
     26
     27        /**
     28         * Lookup of which widgets have had change events triggered.
     29         *
     30         * @var {object}
     31         */
     32        dirtyWidgets: {},
    1333
    1434        init : function() {
    … …  
    3454                });
    3555
     56                // Show AYS dialog when there are unsaved widget changes.
     57                $( window ).on( 'beforeunload.widgets', function( event ) {
     58                        var dirtyWidgetIds = [], unsavedWidgetsElements;
     59                        $.each( self.dirtyWidgets, function( widgetId, dirty ) {
     60                                if ( dirty ) {
     61                                        dirtyWidgetIds.push( widgetId );
     62                                }
     63                        });
     64                        if ( 0 !== dirtyWidgetIds.length ) {
     65                                unsavedWidgetsElements = $( '#widgets-right' ).find( '.widget' ).filter( function() {
     66                                        return -1 !== dirtyWidgetIds.indexOf( $( this ).prop( 'id' ).replace( /^widget-\d+_/, '' ) );
     67                                });
     68                                unsavedWidgetsElements.each( function() {
     69                                        if ( ! $( this ).hasClass( 'open' ) ) {
     70                                                $( this ).find( '.widget-title-action:first' ).click();
     71                                        }
     72                                });
     73
     74                                // Bring the first unsaved widget into view and focus on the first tabbable field.
     75                                unsavedWidgetsElements.first().each( function() {
     76                                        if ( this.scrollIntoViewIfNeeded ) {
     77                                                this.scrollIntoViewIfNeeded();
     78                                        } else {
     79                                                this.scrollIntoView();
     80                                        }
     81                                        $( this ).find( '.widget-inside :tabbable:first' ).focus();
     82                                } );
     83
     84                                event.returnValue = wpWidgets.l10n.saveAlert;
     85                                return event.returnValue;
     86                        }
     87                });
     88
    3689                $('#widgets-left .sidebar-name').click( function() {
    3790                        $(this).closest('.widgets-holder-wrap').toggleClass('closed');
    … …  
    4295                        var target = $(e.target),
    4396                                css = { 'z-index': 100 },
    44                                 widget, inside, targetWidth, widgetWidth, margin,
     97                                widget, inside, targetWidth, widgetWidth, margin, saveButton, widgetId,
    4598                                toggleBtn = target.closest( '.widget' ).find( '.widget-top button.widget-action' );
    4699
    … …  
    48101                                widget = target.closest('div.widget');
    49102                                inside = widget.children('.widget-inside');
    50                                 targetWidth = parseInt( widget.find('input.widget-width').val(), 10 ),
     103                                targetWidth = parseInt( widget.find('input.widget-width').val(), 10 );
    51104                                widgetWidth = widget.parent().width();
     105                                widgetId = inside.find( '.widget-id' ).val();
     106
     107                                // Save button is initially disabled, but is enabled when a field is changed.
     108                                if ( ! widget.data( 'dirty-state-initialized' ) ) {
     109                                        saveButton = inside.find( '.widget-control-save' );
     110                                        saveButton.prop( 'disabled', true ).val( wpWidgets.l10n.saved );
     111                                        inside.on( 'input change', function() {
     112                                                self.dirtyWidgets[ widgetId ] = true;
     113                                                widget.addClass( 'widget-dirty' );
     114                                                saveButton.prop( 'disabled', false ).val( wpWidgets.l10n.save );
     115                                        });
     116                                        widget.data( 'dirty-state-initialized', true );
     117                                }
    52118
    53119                                if ( inside.is(':hidden') ) {
    … …  
    411477
    412478        save : function( widget, del, animate, order ) {
    413                 var sidebarId = widget.closest('div.widgets-sortables').attr('id'),
    414                         data = widget.find('form').serialize(), a;
     479                var self = this, data, a,
     480                        sidebarId = widget.closest( 'div.widgets-sortables' ).attr( 'id' ),
     481                        form = widget.find( 'form' );
     482
     483                if ( form.prop( 'checkValidity' ) && ! form[0].checkValidity() ) {
     484                        return;
     485                }
     486
     487                data = form.serialize();
    415488
    416489                widget = $(widget);
    … …  
    430503
    431504                $.post( ajaxurl, data, function(r) {
    432                         var id;
     505                        var id = $('input.widget-id', widget).val();
    433506
    434507                        if ( del ) {
    435508                                if ( ! $('input.widget_number', widget).val() ) {
    436                                         id = $('input.widget-id', widget).val();
    437509                                        $('#available-widgets').find('input.widget-id').each(function(){
    438510                                                if ( $(this).val() === id ) {
    … …  
    460532                                        $( 'div.widget-content', widget ).html( r );
    461533                                        wpWidgets.appendTitle( widget );
     534
     535                                        // Re-disable the save button.
     536                                        widget.find( '.widget-control-save' ).prop( 'disabled', true ).val( wpWidgets.l10n.saved );
     537
     538                                        widget.removeClass( 'widget-dirty' );
     539
     540                                        // Clear the dirty flag from the widget.
     541                                        delete self.dirtyWidgets[ id ];
     542
    462543                                        $document.trigger( 'widget-updated', [ widget ] );
    463544
  • trunk/src/wp-admin/js/widgets/text-widgets.js

    r41350 r41352  
    166166                 */
    167167                initializeEditor: function initializeEditor() {
    168                         var control = this, changeDebounceDelay = 1000, id, textarea, triggerChangeIfDirty, restoreTextMode = false, needsTextareaChangeTrigger = false;
     168                        var control = this, changeDebounceDelay = 1000, id, textarea, triggerChangeIfDirty, restoreTextMode = false, needsTextareaChangeTrigger = false, previousValue;
    169169                        textarea = control.fields.text;
    170170                        id = textarea.attr( 'id' );
     171                        previousValue = textarea.val();
    171172
    172173                        /**
    … …  
    203204                                }
    204205
    205                                 // Trigger change on textarea when it is dirty for sake of widgets in the Customizer needing to sync form inputs to setting models.
    206                                 if ( needsTextareaChangeTrigger ) {
     206                                // Trigger change on textarea when it has changed so the widget can enter a dirty state.
     207                                if ( needsTextareaChangeTrigger && previousValue !== textarea.val() ) {
    207208                                        textarea.trigger( 'change' );
    208209                                        needsTextareaChangeTrigger = false;
     210                                        previousValue = textarea.val();
    209211                                }
    210212                        };
  • trunk/src/wp-includes/script-loader.php

    r41329 r41352  
    673673
    674674                $scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
     675                $scripts->add_inline_script( 'admin-widgets', sprintf( 'wpWidgets.l10n = %s;', wp_json_encode( array(
     676                        'save' => __( 'Save' ),
     677                        'saved' => __( 'Saved' ),
     678                        'saveAlert' => __( 'The changes you made will be lost if you navigate away from this page.' ),
     679                ) ) ) );
     680
    675681                $scripts->add( 'media-widgets', "/wp-admin/js/widgets/media-widgets$suffix.js", array( 'jquery', 'media-models', 'media-views', 'wp-api-request' ) );
    676682                $scripts->add_inline_script( 'media-widgets', 'wp.mediaWidgets.init();', 'after' );
Note: See TracChangeset for help on using the changeset viewer.