Make WordPress Core

Changeset 41352


Ignore:
Timestamp:
09/08/2017 07:10:59 PM (7 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.