wp/wp-admin/js/widgets.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
--- a/wp/wp-admin/js/widgets.js	Tue Jun 09 11:14:17 2015 +0000
+++ b/wp/wp-admin/js/widgets.js	Mon Oct 14 17:39:30 2019 +0200
@@ -1,8 +1,35 @@
 /*global ajaxurl, isRtl */
 var wpWidgets;
 (function($) {
+	var $document = $( document );
 
 wpWidgets = {
+	/**
+	 * A closed Sidebar that gets a Widget dragged over it.
+	 *
+	 * @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,
@@ -12,32 +39,112 @@
 			sidebars = $('div.widgets-sortables'),
 			isRTL = !! ( 'undefined' !== typeof isRtl && isRtl );
 
-		$('#widgets-right .sidebar-name').click( function() {
-			var $this = $(this),
-				$wrap = $this.closest('.widgets-holder-wrap');
+		// Handle the widgets containers in the right column.
+		$( '#widgets-right .sidebar-name' )
+			/*
+			 * Toggle the widgets containers when clicked and update the toggle
+			 * button `aria-expanded` attribute value.
+			 */
+			.click( function() {
+				var $this = $( this ),
+					$wrap = $this.closest( '.widgets-holder-wrap '),
+					$toggle = $this.find( '.handlediv' );
+
+				if ( $wrap.hasClass( 'closed' ) ) {
+					$wrap.removeClass( 'closed' );
+					$toggle.attr( 'aria-expanded', 'true' );
+					// Refresh the jQuery UI sortable items.
+					$this.parent().sortable( 'refresh' );
+				} else {
+					$wrap.addClass( 'closed' );
+					$toggle.attr( 'aria-expanded', 'false' );
+				}
+
+				// Update the admin menu "sticky" state.
+				$document.triggerHandler( 'wp-pin-menu' );
+			})
+			/*
+			 * Set the initial `aria-expanded` attribute value on the widgets
+			 * containers toggle button. The first one is expanded by default.
+			 */
+			.find( '.handlediv' ).each( function( index ) {
+				if ( 0 === index ) {
+					// jQuery equivalent of `continue` within an `each()` loop.
+					return;
+				}
 
-			if ( $wrap.hasClass('closed') ) {
-				$wrap.removeClass('closed');
-				$this.parent().sortable('refresh');
-			} else {
-				$wrap.addClass('closed');
+				$( this ).attr( 'aria-expanded', 'false' );
+			});
+
+		// 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');
+		// Handle the widgets containers in the left column.
+		$( '#widgets-left .sidebar-name' ).click( function() {
+			var $wrap = $( this ).closest( '.widgets-holder-wrap' );
+
+			$wrap
+				.toggleClass( 'closed' )
+				.find( '.handlediv' ).attr( 'aria-expanded', ! $wrap.hasClass( 'closed' ) );
+
+			// Update the admin menu "sticky" state.
+			$document.triggerHandler( 'wp-pin-menu' );
 		});
 
 		$(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 ) {
@@ -50,10 +157,21 @@
 						css[ margin ] = widgetWidth - ( targetWidth + 30 ) + 'px';
 						widget.css( css );
 					}
-					widget.addClass( 'open' );
-					inside.slideDown('fast');
+					/*
+					 * Don't change the order of attributes changes and animation:
+					 * it's important for screen readers, see ticket #31476.
+					 */
+					toggleBtn.attr( 'aria-expanded', 'true' );
+					inside.slideDown( 'fast', function() {
+						widget.addClass( 'open' );
+					});
 				} else {
-					inside.slideUp('fast', function() {
+					/*
+					 * Don't change the order of attributes changes and animation:
+					 * it's important for screen readers, see ticket #31476.
+					 */
+					toggleBtn.attr( 'aria-expanded', 'false' );
+					inside.slideUp( 'fast', function() {
 						widget.attr( 'style', '' );
 						widget.removeClass( 'open' );
 					});
@@ -68,8 +186,12 @@
 			} else if ( target.hasClass('widget-control-close') ) {
 				widget = target.closest('div.widget');
 				widget.removeClass( 'open' );
+				toggleBtn.attr( 'aria-expanded', 'false' );
 				wpWidgets.close( widget );
 				e.preventDefault();
+			} else if ( target.attr( 'id' ) === 'inactive-widgets-control-remove' ) {
+				wpWidgets.removeInactiveWidgets();
+				e.preventDefault();
 			}
 		});
 
@@ -79,7 +201,7 @@
 			wpWidgets.appendTitle( this );
 
 			if ( $this.find( 'p.widget-error' ).length ) {
-				$this.find( 'a.widget-action' ).trigger('click');
+				$this.find( '.widget-action' ).trigger( 'click' ).attr( 'aria-expanded', 'true' );
 			}
 		});
 
@@ -89,7 +211,8 @@
 			distance: 2,
 			helper: 'clone',
 			zIndex: 100,
-			containment: 'document',
+			containment: '#wpwrap',
+			refreshPositions: true,
 			start: function( event, ui ) {
 				var chooser = $(this).find('.widgets-chooser');
 
@@ -113,19 +236,64 @@
 			}
 		});
 
+		/**
+		 * Opens and closes previously closed Sidebars when Widgets are dragged over/out of them.
+		 */
+		sidebars.droppable( {
+			tolerance: 'intersect',
+
+			/**
+			 * Open Sidebar when a Widget gets dragged over it.
+			 *
+			 * @param {object} event jQuery event object.
+			 */
+			over: function( event ) {
+				var $wrap = $( event.target ).parent();
+
+				if ( wpWidgets.hoveredSidebar && ! $wrap.is( wpWidgets.hoveredSidebar ) ) {
+					// Close the previous Sidebar as the Widget has been dragged onto another Sidebar.
+					wpWidgets.closeSidebar( event );
+				}
+
+				if ( $wrap.hasClass( 'closed' ) ) {
+					wpWidgets.hoveredSidebar = $wrap;
+					$wrap
+						.removeClass( 'closed' )
+						.find( '.handlediv' ).attr( 'aria-expanded', 'true' );
+				}
+
+				$( this ).sortable( 'refresh' );
+			},
+
+			/**
+			 * Close Sidebar when the Widget gets dragged out of it.
+			 *
+			 * @param {object} event jQuery event object.
+			 */
+			out: function( event ) {
+				if ( wpWidgets.hoveredSidebar ) {
+					wpWidgets.closeSidebar( event );
+				}
+			}
+		} );
+
 		sidebars.sortable({
 			placeholder: 'widget-placeholder',
 			items: '> .widget',
 			handle: '> .widget-top > .widget-title',
 			cursor: 'move',
 			distance: 2,
-			containment: 'document',
+			containment: '#wpwrap',
+			tolerance: 'pointer',
+			refreshPositions: true,
 			start: function( event, ui ) {
 				var height, $this = $(this),
 					$wrap = $this.parent(),
 					inside = ui.item.children('.widget-inside');
 
 				if ( inside.css('display') === 'block' ) {
+					ui.item.removeClass('open');
+					ui.item.find( '.widget-top button.widget-action' ).attr( 'aria-expanded', 'false' );
 					inside.hide();
 					$(this).sortable('refreshPositions');
 				}
@@ -143,6 +311,9 @@
 					$widget = ui.item,
 					id = the_id;
 
+				// Reset the var to hold a previously closed sidebar.
+				wpWidgets.hoveredSidebar = null;
+
 				if ( $widget.hasClass('deleting') ) {
 					wpWidgets.save( $widget, 1, 0, 1 ); // delete widget
 					$widget.remove();
@@ -174,13 +345,16 @@
 
 					wpWidgets.save( $widget, 0, 0, 1 );
 					$widget.find('input.add_new').val('');
-					$( document ).trigger( 'widget-added', [ $widget ] );
+					$document.trigger( 'widget-added', [ $widget ] );
 				}
 
 				$sidebar = $widget.parent();
 
 				if ( $sidebar.parent().hasClass('closed') ) {
-					$sidebar.parent().removeClass('closed');
+					$sidebar.parent()
+						.removeClass( 'closed' )
+						.find( '.handlediv' ).attr( 'aria-expanded', 'true' );
+
 					$children = $sidebar.children('.widget');
 
 					// Make sure the dropped widget is at the top
@@ -195,7 +369,7 @@
 				}
 
 				if ( addNew ) {
-					$widget.find( 'a.widget-action' ).trigger('click');
+					$widget.find( '.widget-action' ).trigger( 'click' );
 				} else {
 					wpWidgets.saveOrder( $sidebar.attr('id') );
 				}
@@ -241,7 +415,7 @@
 
 				if ( ui.draggable.hasClass('ui-sortable-helper') ) {
 					$('#removing-widget').show().children('span')
-					.html( ui.draggable.find('div.widget-title').children('h4').html() );
+					.html( ui.draggable.find( 'div.widget-title' ).children( 'h3' ).html() );
 				}
 			},
 			out: function(e,ui) {
@@ -254,7 +428,7 @@
 		// Area Chooser
 		$( '#widgets-right .widgets-holder-wrap' ).each( function( index, element ) {
 			var $element = $( element ),
-				name = $element.find( '.sidebar-name h3' ).text(),
+				name = $element.find( '.sidebar-name h2' ).text(),
 				id = $element.find( '.widgets-sortables' ).attr( 'id' ),
 				li = $('<li tabindex="0">').text( $.trim( name ) );
 
@@ -295,12 +469,12 @@
 			if ( $target.hasClass('button-primary') ) {
 				self.addWidget( chooser );
 				self.closeChooser();
-			} else if ( $target.hasClass('button-secondary') ) {
+			} else if ( $target.hasClass( 'widgets-chooser-cancel' ) ) {
 				self.closeChooser();
 			}
 		}).on( 'keyup.widgets-chooser', function( event ) {
 			if ( event.which === $.ui.keyCode.ENTER ) {
-				if ( $( event.target ).hasClass('button-secondary') ) {
+				if ( $( event.target ).hasClass( 'widgets-chooser-cancel' ) ) {
 					// Close instead of adding when pressing Enter on the Cancel button
 					self.closeChooser();
 				} else {
@@ -331,13 +505,22 @@
 		});
 
 		$.post( ajaxurl, data, function() {
+			$( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length );
 			$( '.spinner' ).removeClass( 'is-active' );
 		});
 	},
 
 	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' ),
+			isAdd = widget.find( 'input.add_new' ).val();
+
+		if ( ! del && ! isAdd && form.prop( 'checkValidity' ) && ! form[0].checkValidity() ) {
+			return;
+		}
+
+		data = form.serialize();
 
 		widget = $(widget);
 		$( '.spinner', widget ).addClass( 'is-active' );
@@ -355,11 +538,10 @@
 		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();
@@ -369,27 +551,70 @@
 
 				if ( animate ) {
 					order = 0;
-					widget.slideUp('fast', function(){
-						$(this).remove();
+					widget.slideUp( 'fast', function() {
+						$( this ).remove();
 						wpWidgets.saveOrder();
+						delete self.dirtyWidgets[ id ];
 					});
 				} else {
 					widget.remove();
+					delete self.dirtyWidgets[ id ];
+
+					if ( sidebarId === 'wp_inactive_widgets' ) {
+						$( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length );
+					}
 				}
 			} else {
 				$( '.spinner' ).removeClass( 'is-active' );
 				if ( r && r.length > 2 ) {
 					$( 'div.widget-content', widget ).html( r );
 					wpWidgets.appendTitle( widget );
-					$( document ).trigger( 'widget-updated', [ 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' ) {
+						$( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length );
+					}
 				}
 			}
+
 			if ( order ) {
 				wpWidgets.saveOrder();
 			}
 		});
 	},
 
+	removeInactiveWidgets : function() {
+		var $element = $( '.remove-inactive-widgets' ), self = this, a, data;
+
+		$( '.spinner', $element ).addClass( 'is-active' );
+
+		a = {
+			action : 'delete-inactive-widgets',
+			removeinactivewidgets : $( '#_wpnonce_remove_inactive_widgets' ).val()
+		};
+
+		data = $.param( a );
+
+		$.post( ajaxurl, data, function() {
+			$( '#wp_inactive_widgets .widget' ).each(function() {
+				var $widget = $( this );
+				delete self.dirtyWidgets[ $widget.find( 'input.widget-id' ).val() ];
+				$widget.remove();
+			});
+			$( '#inactive-widgets-control-remove' ).prop( 'disabled', true );
+			$( '.spinner', $element ).removeClass( 'is-active' );
+		} );
+	},
+
 	appendTitle : function(widget) {
 		var title = $('input[id*="-title"]', widget).val() || '';
 
@@ -404,7 +629,10 @@
 
 	close : function(widget) {
 		widget.children('.widget-inside').slideUp('fast', function() {
-			widget.attr( 'style', '' );
+			widget.attr( 'style', '' )
+				.find( '.widget-top button.widget-action' )
+					.attr( 'aria-expanded', 'false' )
+					.focus();
 		});
 	},
 
@@ -436,8 +664,10 @@
 			$( '#' + widgetId ).hide();
 		}
 
-		// Open the widgets container
-		sidebar.closest( '.widgets-holder-wrap' ).removeClass('closed');
+		// Open the widgets container.
+		sidebar.closest( '.widgets-holder-wrap' )
+			.removeClass( 'closed' )
+			.find( '.handlediv' ).attr( 'aria-expanded', 'true' );
 
 		sidebar.append( widget );
 		sidebar.sortable('refresh');
@@ -446,7 +676,7 @@
 		// No longer "new" widget
 		widget.find( 'input.add_new' ).val('');
 
-		$( document ).trigger( 'widget-added', [ widget ] );
+		$document.trigger( 'widget-added', [ widget ] );
 
 		/*
 		 * Check if any part of the sidebar is visible in the viewport. If it is, don't scroll.
@@ -486,9 +716,25 @@
 	clearWidgetSelection: function() {
 		$( '#widgets-left' ).removeClass( 'chooser' );
 		$( '.widget-in-question' ).removeClass( 'widget-in-question' );
+	},
+
+	/**
+	 * Closes a Sidebar that was previously closed, but opened by dragging a Widget over it.
+	 *
+	 * Used when a Widget gets dragged in/out of the Sidebar and never dropped.
+	 *
+	 * @param {object} event jQuery event object.
+	 */
+	closeSidebar: function( event ) {
+		this.hoveredSidebar
+			.addClass( 'closed' )
+			.find( '.handlediv' ).attr( 'aria-expanded', 'false' );
+
+		$( event.target ).css( 'min-height', '' );
+		this.hoveredSidebar = null;
 	}
 };
 
-$(document).ready( function(){ wpWidgets.init(); } );
+$document.ready( function(){ wpWidgets.init(); } );
 
 })(jQuery);