diff --git a/src/wp-admin/css/widgets.css b/src/wp-admin/css/widgets.css index 5600a785bb..10d0ac8958 100644 --- a/src/wp-admin/css/widgets.css +++ b/src/wp-admin/css/widgets.css @@ -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 { diff --git a/src/wp-admin/includes/widgets.php b/src/wp-admin/includes/widgets.php index e6af6e7677..d599e5daba 100644 --- a/src/wp-admin/includes/widgets.php +++ b/src/wp-admin/includes/widgets.php @@ -253,8 +253,11 @@ function wp_widget_control( $sidebar_args ) {
- | - + + + | + +
'widget-' . esc_attr( $id_format ) . '-savewidget' ) ); ?> diff --git a/src/wp-admin/js/widgets.js b/src/wp-admin/js/widgets.js index 77cf8f92fa..5866795d1a 100644 --- a/src/wp-admin/js/widgets.js +++ b/src/wp-admin/js/widgets.js @@ -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' ) { diff --git a/src/wp-admin/js/widgets/text-widgets.js b/src/wp-admin/js/widgets/text-widgets.js index 0d790afe3a..2584945f9a 100644 --- a/src/wp-admin/js/widgets/text-widgets.js +++ b/src/wp-admin/js/widgets/text-widgets.js @@ -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(); } }; diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 6e83b57d84..137f74765b 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -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' );