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.

Built from https://develop.svn.wordpress.org/trunk@41352


git-svn-id: http://core.svn.wordpress.org/trunk@41185 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Weston Ruter 2017-09-08 19:11:43 +00:00
parent 8a23b80b56
commit 9b5d71bad0
11 changed files with 117 additions and 17 deletions

View File

@ -42,6 +42,10 @@
line-height: 16px;
}
.widget.widget-dirty .widget-control-close-wrapper {
display: none;
}
.in-widget-title,
#widgets-right a.widget-control-edit,
#available-widgets .widget-description {

File diff suppressed because one or more lines are too long

View File

@ -42,6 +42,10 @@
line-height: 16px;
}
.widget.widget-dirty .widget-control-close-wrapper {
display: none;
}
.in-widget-title,
#widgets-right a.widget-control-edit,
#available-widgets .widget-description {

File diff suppressed because one or more lines are too long

View File

@ -253,8 +253,11 @@ function wp_widget_control( $sidebar_args ) {
<div class="widget-control-actions">
<div class="alignleft">
<button type="button" class="button-link button-link-delete widget-control-remove"><?php _e( 'Delete' ); ?></button> |
<button type="button" class="button-link widget-control-close"><?php _e( 'Close' ); ?></button>
<button type="button" class="button-link button-link-delete widget-control-remove"><?php _e( 'Delete' ); ?></button>
<span class="widget-control-close-wrapper">
|
<button type="button" class="button-link widget-control-close"><?php _e( 'Done' ); ?></button>
</span>
</div>
<div class="alignright<?php if ( 'noform' === $has_form ) echo ' widget-control-noform'; ?>">
<?php submit_button( __( 'Save' ), 'primary widget-control-save right', 'savewidget', false, array( 'id' => 'widget-' . esc_attr( $id_format ) . '-savewidget' ) ); ?>

View File

@ -7,10 +7,30 @@ wpWidgets = {
/**
* A closed Sidebar that gets a Widget dragged over it.
*
* @var element|null
* @var {element|null}
*/
hoveredSidebar: null,
/**
* Translations.
*
* Exported from PHP in wp_default_scripts().
*
* @var {object}
*/
l10n: {
save: '{save}',
saved: '{saved}',
saveAlert: '{saveAlert}'
},
/**
* Lookup of which widgets have had change events triggered.
*
* @var {object}
*/
dirtyWidgets: {},
init : function() {
var rem, the_id,
self = this,
@ -33,6 +53,39 @@ wpWidgets = {
$document.triggerHandler( 'wp-pin-menu' );
});
// Show AYS dialog when there are unsaved widget changes.
$( window ).on( 'beforeunload.widgets', function( event ) {
var dirtyWidgetIds = [], unsavedWidgetsElements;
$.each( self.dirtyWidgets, function( widgetId, dirty ) {
if ( dirty ) {
dirtyWidgetIds.push( widgetId );
}
});
if ( 0 !== dirtyWidgetIds.length ) {
unsavedWidgetsElements = $( '#widgets-right' ).find( '.widget' ).filter( function() {
return -1 !== dirtyWidgetIds.indexOf( $( this ).prop( 'id' ).replace( /^widget-\d+_/, '' ) );
});
unsavedWidgetsElements.each( function() {
if ( ! $( this ).hasClass( 'open' ) ) {
$( this ).find( '.widget-title-action:first' ).click();
}
});
// Bring the first unsaved widget into view and focus on the first tabbable field.
unsavedWidgetsElements.first().each( function() {
if ( this.scrollIntoViewIfNeeded ) {
this.scrollIntoViewIfNeeded();
} else {
this.scrollIntoView();
}
$( this ).find( '.widget-inside :tabbable:first' ).focus();
} );
event.returnValue = wpWidgets.l10n.saveAlert;
return event.returnValue;
}
});
$('#widgets-left .sidebar-name').click( function() {
$(this).closest('.widgets-holder-wrap').toggleClass('closed');
$document.triggerHandler( 'wp-pin-menu' );
@ -41,14 +94,27 @@ wpWidgets = {
$(document.body).bind('click.widgets-toggle', function(e) {
var target = $(e.target),
css = { 'z-index': 100 },
widget, inside, targetWidth, widgetWidth, margin,
widget, inside, targetWidth, widgetWidth, margin, saveButton, widgetId,
toggleBtn = target.closest( '.widget' ).find( '.widget-top button.widget-action' );
if ( target.parents('.widget-top').length && ! target.parents('#available-widgets').length ) {
widget = target.closest('div.widget');
inside = widget.children('.widget-inside');
targetWidth = parseInt( widget.find('input.widget-width').val(), 10 ),
targetWidth = parseInt( widget.find('input.widget-width').val(), 10 );
widgetWidth = widget.parent().width();
widgetId = inside.find( '.widget-id' ).val();
// Save button is initially disabled, but is enabled when a field is changed.
if ( ! widget.data( 'dirty-state-initialized' ) ) {
saveButton = inside.find( '.widget-control-save' );
saveButton.prop( 'disabled', true ).val( wpWidgets.l10n.saved );
inside.on( 'input change', function() {
self.dirtyWidgets[ widgetId ] = true;
widget.addClass( 'widget-dirty' );
saveButton.prop( 'disabled', false ).val( wpWidgets.l10n.save );
});
widget.data( 'dirty-state-initialized', true );
}
if ( inside.is(':hidden') ) {
if ( targetWidth > 250 && ( targetWidth + 30 > widgetWidth ) && widget.closest('div.widgets-sortables').length ) {
@ -410,8 +476,15 @@ wpWidgets = {
},
save : function( widget, del, animate, order ) {
var sidebarId = widget.closest('div.widgets-sortables').attr('id'),
data = widget.find('form').serialize(), a;
var self = this, data, a,
sidebarId = widget.closest( 'div.widgets-sortables' ).attr( 'id' ),
form = widget.find( 'form' );
if ( form.prop( 'checkValidity' ) && ! form[0].checkValidity() ) {
return;
}
data = form.serialize();
widget = $(widget);
$( '.spinner', widget ).addClass( 'is-active' );
@ -429,11 +502,10 @@ wpWidgets = {
data += '&' + $.param(a);
$.post( ajaxurl, data, function(r) {
var id;
var id = $('input.widget-id', widget).val();
if ( del ) {
if ( ! $('input.widget_number', widget).val() ) {
id = $('input.widget-id', widget).val();
$('#available-widgets').find('input.widget-id').each(function(){
if ( $(this).val() === id ) {
$(this).closest('div.widget').show();
@ -459,6 +531,15 @@ wpWidgets = {
if ( r && r.length > 2 ) {
$( 'div.widget-content', widget ).html( r );
wpWidgets.appendTitle( widget );
// Re-disable the save button.
widget.find( '.widget-control-save' ).prop( 'disabled', true ).val( wpWidgets.l10n.saved );
widget.removeClass( 'widget-dirty' );
// Clear the dirty flag from the widget.
delete self.dirtyWidgets[ id ];
$document.trigger( 'widget-updated', [ widget ] );
if ( sidebarId === 'wp_inactive_widgets' ) {

File diff suppressed because one or more lines are too long

View File

@ -165,9 +165,10 @@ wp.textWidgets = ( function( $ ) {
* @returns {void}
*/
initializeEditor: function initializeEditor() {
var control = this, changeDebounceDelay = 1000, id, textarea, triggerChangeIfDirty, restoreTextMode = false, needsTextareaChangeTrigger = false;
var control = this, changeDebounceDelay = 1000, id, textarea, triggerChangeIfDirty, restoreTextMode = false, needsTextareaChangeTrigger = false, previousValue;
textarea = control.fields.text;
id = textarea.attr( 'id' );
previousValue = textarea.val();
/**
* Trigger change if dirty.
@ -202,10 +203,11 @@ wp.textWidgets = ( function( $ ) {
}
}
// Trigger change on textarea when it is dirty for sake of widgets in the Customizer needing to sync form inputs to setting models.
if ( needsTextareaChangeTrigger ) {
// Trigger change on textarea when it has changed so the widget can enter a dirty state.
if ( needsTextareaChangeTrigger && previousValue !== textarea.val() ) {
textarea.trigger( 'change' );
needsTextareaChangeTrigger = false;
previousValue = textarea.val();
}
};

File diff suppressed because one or more lines are too long

View File

@ -672,6 +672,12 @@ function wp_default_scripts( &$scripts ) {
$scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
$scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
$scripts->add_inline_script( 'admin-widgets', sprintf( 'wpWidgets.l10n = %s;', wp_json_encode( array(
'save' => __( 'Save' ),
'saved' => __( 'Saved' ),
'saveAlert' => __( 'The changes you made will be lost if you navigate away from this page.' ),
) ) ) );
$scripts->add( 'media-widgets', "/wp-admin/js/widgets/media-widgets$suffix.js", array( 'jquery', 'media-models', 'media-views', 'wp-api-request' ) );
$scripts->add_inline_script( 'media-widgets', 'wp.mediaWidgets.init();', 'after' );

View File

@ -4,7 +4,7 @@
*
* @global string $wp_version
*/
$wp_version = '4.9-alpha-41351';
$wp_version = '4.9-alpha-41352';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.