wp/wp-admin/js/image-edit.js
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
--- a/wp/wp-admin/js/image-edit.js	Thu Sep 29 08:06:27 2022 +0200
+++ b/wp/wp-admin/js/image-edit.js	Fri Sep 05 18:40:08 2025 +0200
@@ -11,7 +11,7 @@
 	var __ = wp.i18n.__;
 
 	/**
-	 * Contains all the methods to initialise and control the image editor.
+	 * Contains all the methods to initialize and control the image editor.
 	 *
 	 * @namespace imageEdit
 	 */
@@ -22,25 +22,61 @@
 	_view : false,
 
 	/**
-	 * Handle crop tool clicks.
+	 * Enable crop tool.
 	 */
-	handleCropToolClick: function( postid, nonce, cropButton ) {
+	toggleCropTool: function( postid, nonce, cropButton ) {
 		var img = $( '#image-preview-' + postid ),
 			selection = this.iasapi.getSelection();
 
-		// Ensure selection is available, otherwise reset to full image.
-		if ( isNaN( selection.x1 ) ) {
-			this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': img.innerWidth(), 'y2': img.innerHeight(), 'width': img.innerWidth(), 'height': img.innerHeight() } );
-			selection = this.iasapi.getSelection();
-		}
+		imageEdit.toggleControls( cropButton );
+		var $el = $( cropButton );
+		var state = ( $el.attr( 'aria-expanded' ) === 'true' ) ? 'true' : 'false';
+		// Crop tools have been closed.
+		if ( 'false' === state ) {
+			// Cancel selection, but do not unset inputs.
+			this.iasapi.cancelSelection();
+			imageEdit.setDisabled($('.imgedit-crop-clear'), 0);
+		} else {
+			imageEdit.setDisabled($('.imgedit-crop-clear'), 1);
+			// Get values from inputs to restore previous selection.
+			var startX = ( $( '#imgedit-start-x-' + postid ).val() ) ? $('#imgedit-start-x-' + postid).val() : 0;
+			var startY = ( $( '#imgedit-start-y-' + postid ).val() ) ? $('#imgedit-start-y-' + postid).val() : 0;
+			var width = ( $( '#imgedit-sel-width-' + postid ).val() ) ? $('#imgedit-sel-width-' + postid).val() : img.innerWidth();
+			var height = ( $( '#imgedit-sel-height-' + postid ).val() ) ? $('#imgedit-sel-height-' + postid).val() : img.innerHeight();
+			// Ensure selection is available, otherwise reset to full image.
+			if ( isNaN( selection.x1 ) ) {
+				this.setCropSelection( postid, { 'x1': startX, 'y1': startY, 'x2': width, 'y2': height, 'width': width, 'height': height } );
+				selection = this.iasapi.getSelection();
+			}
 
-		// If we don't already have a selection, select the entire image.
-		if ( 0 === selection.x1 && 0 === selection.y1 && 0 === selection.x2 && 0 === selection.y2 ) {
-			this.iasapi.setSelection( 0, 0, img.innerWidth(), img.innerHeight(), true );
-			this.iasapi.setOptions( { show: true } );
-			this.iasapi.update();
+			// If we don't already have a selection, select the entire image.
+			if ( 0 === selection.x1 && 0 === selection.y1 && 0 === selection.x2 && 0 === selection.y2 ) {
+				this.iasapi.setSelection( 0, 0, img.innerWidth(), img.innerHeight(), true );
+				this.iasapi.setOptions( { show: true } );
+				this.iasapi.update();
+			} else {
+				this.iasapi.setSelection( startX, startY, width, height, true );
+				this.iasapi.setOptions( { show: true } );
+				this.iasapi.update();
+			}
+		}
+	},
+
+	/**
+	 * Handle crop tool clicks.
+	 */
+	handleCropToolClick: function( postid, nonce, cropButton ) {
+
+		if ( cropButton.classList.contains( 'imgedit-crop-clear' ) ) {
+			this.iasapi.cancelSelection();
+			imageEdit.setDisabled($('.imgedit-crop-apply'), 0);
+
+			$('#imgedit-sel-width-' + postid).val('');
+			$('#imgedit-sel-height-' + postid).val('');
+			$('#imgedit-start-x-' + postid).val('0');
+			$('#imgedit-start-y-' + postid).val('0');
+			$('#imgedit-selection-' + postid).val('');
 		} else {
-
 			// Otherwise, perform the crop.
 			imageEdit.crop( postid, nonce , cropButton );
 		}
@@ -122,6 +158,17 @@
 		t.postid = postid;
 		$('#imgedit-response-' + postid).empty();
 
+		$('#imgedit-panel-' + postid).on( 'keypress', function(e) {
+			var nonce = $( '#imgedit-nonce-' + postid ).val();
+			if ( e.which === 26 && e.ctrlKey ) {
+				imageEdit.undo( postid, nonce );
+			}
+
+			if ( e.which === 25 && e.ctrlKey ) {
+				imageEdit.redo( postid, nonce );
+			}
+		});
+
 		$('#imgedit-panel-' + postid).on( 'keypress', 'input[type="text"]', function(e) {
 			var k = e.keyCode;
 
@@ -170,6 +217,125 @@
 	},
 
 	/**
+	 * Shows or hides image menu popup.
+	 *
+	 * @since 6.3.0
+	 *
+	 * @memberof imageEdit
+	 *
+	 * @param {HTMLElement} el The activated control element.
+	 *
+	 * @return {boolean} Always returns false.
+	 */
+	togglePopup : function(el) {
+		var $el = $( el );
+		var $targetEl = $( el ).attr( 'aria-controls' );
+		var $target = $( '#' + $targetEl );
+		$el
+			.attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' );
+		// Open menu and set z-index to appear above image crop area if it is enabled.
+		$target
+			.toggleClass( 'imgedit-popup-menu-open' ).slideToggle( 'fast' ).css( { 'z-index' : 200000 } );
+		// Move focus to first item in menu when opening menu.
+		if ( 'true' === $el.attr( 'aria-expanded' ) ) {
+			$target.find( 'button' ).first().trigger( 'focus' );
+		}
+
+		return false;
+	},
+
+	/**
+	 * Observes whether the popup should remain open based on focus position.
+	 *
+	 * @since 6.4.0
+	 *
+	 * @memberof imageEdit
+	 *
+	 * @param {HTMLElement} el The activated control element.
+	 *
+	 * @return {boolean} Always returns false.
+	 */
+	monitorPopup : function() {
+		var $parent = document.querySelector( '.imgedit-rotate-menu-container' );
+		var $toggle = document.querySelector( '.imgedit-rotate-menu-container .imgedit-rotate' );
+
+		setTimeout( function() {
+			var $focused = document.activeElement;
+			var $contains = $parent.contains( $focused );
+
+			// If $focused is defined and not inside the menu container, close the popup.
+			if ( $focused && ! $contains ) {
+				if ( 'true' === $toggle.getAttribute( 'aria-expanded' ) ) {
+					imageEdit.togglePopup( $toggle );
+				}
+			}
+		}, 100 );
+
+		return false;
+	},
+
+	/**
+	 * Navigate popup menu by arrow keys.
+	 *
+	 * @since 6.3.0
+	 *
+	 * @memberof imageEdit
+	 *
+	 * @param {HTMLElement} el The current element.
+	 *
+	 * @return {boolean} Always returns false.
+	 */
+	browsePopup : function(el) {
+		var $el = $( el );
+		var $collection = $( el ).parent( '.imgedit-popup-menu' ).find( 'button' );
+		var $index = $collection.index( $el );
+		var $prev = $index - 1;
+		var $next = $index + 1;
+		var $last = $collection.length;
+		if ( $prev < 0 ) {
+			$prev = $last - 1;
+		}
+		if ( $next === $last ) {
+			$next = 0;
+		}
+		var $target = false;
+		if ( event.keyCode === 40 ) {
+			$target = $collection.get( $next );
+		} else if ( event.keyCode === 38 ) {
+			$target = $collection.get( $prev );
+		}
+		if ( $target ) {
+			$target.focus();
+			event.preventDefault();
+		}
+
+		return false;
+	},
+
+	/**
+	 * Close popup menu and reset focus on feature activation.
+	 *
+	 * @since 6.3.0
+	 *
+	 * @memberof imageEdit
+	 *
+	 * @param {HTMLElement} el The current element.
+	 *
+	 * @return {boolean} Always returns false.
+	 */
+	closePopup : function(el) {
+		var $parent = $(el).parent( '.imgedit-popup-menu' );
+		var $controlledID = $parent.attr( 'id' );
+		var $target = $( 'button[aria-controls="' + $controlledID + '"]' );
+		$target
+			.attr( 'aria-expanded', 'false' ).trigger( 'focus' );
+		$parent
+			.toggleClass( 'imgedit-popup-menu-open' ).slideToggle( 'fast' );
+
+		return false;
+	},
+
+	/**
 	 * Shows or hides the image edit help box.
 	 *
 	 * @since 2.9.0
@@ -190,6 +356,28 @@
 	},
 
 	/**
+	 * Shows or hides image edit input fields when enabled.
+	 *
+	 * @since 6.3.0
+	 *
+	 * @memberof imageEdit
+	 *
+	 * @param {HTMLElement} el The element to trigger the edit panel.
+	 *
+	 * @return {boolean} Always returns false.
+	 */
+	toggleControls : function(el) {
+		var $el = $( el );
+		var $target = $( '#' + $el.attr( 'aria-controls' ) );
+		$el
+			.attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' );
+		$target
+			.parent( '.imgedit-group' ).toggleClass( 'imgedit-panel-active' );
+
+		return false;
+	},
+
+	/**
 	 * Gets the value from the image edit target.
 	 *
 	 * The image edit target contains the image sizes where the (possible) changes
@@ -202,10 +390,16 @@
 	 * @param {number} postid The post ID.
 	 *
 	 * @return {string} The value from the imagedit-save-target input field when available,
-	 *                  or 'full' when not available.
+	 *                  'full' when not selected, or 'all' if it doesn't exist.
 	 */
-	getTarget : function(postid) {
-		return $('input[name="imgedit-target-' + postid + '"]:checked', '#imgedit-save-target-' + postid).val() || 'full';
+	getTarget : function( postid ) {
+		var element = $( '#imgedit-save-target-' + postid );
+
+		if ( element.length ) {
+			return element.find( 'input[name="imgedit-target-' + postid + '"]:checked' ).val() || 'full';
+		}
+
+		return 'all';
 	},
 
 	/**
@@ -226,7 +420,8 @@
 	 */
 	scaleChanged : function( postid, x, el ) {
 		var w = $('#imgedit-scale-width-' + postid), h = $('#imgedit-scale-height-' + postid),
-		warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = '';
+		warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = '',
+		scaleBtn = $('#imgedit-scale-button');
 
 		if ( false === this.validateNumeric( el ) ) {
 			return;
@@ -242,8 +437,10 @@
 
 		if ( ( h1 && h1 > this.hold.oh ) || ( w1 && w1 > this.hold.ow ) ) {
 			warn.css('visibility', 'visible');
+			scaleBtn.prop('disabled', true);
 		} else {
 			warn.css('visibility', 'hidden');
+			scaleBtn.prop('disabled', false);
 		}
 	},
 
@@ -402,12 +599,14 @@
 				}
 
 				if ( $('#imgedit-history-' + postid).val() && $('#imgedit-undone-' + postid).val() === '0' ) {
-					$('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false);
+					$('button.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false);
 				} else {
-					$('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', true);
+					$('button.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', true);
 				}
+				var successMessage = __( 'Image updated.' );
 
 				t.toggleEditor(postid, 0);
+				wp.a11y.speak( successMessage, 'assertive' );
 			})
 			.on( 'error', function() {
 				var errorMessage = __( 'Could not load the preview image. Please reload the page and try again.' );
@@ -435,7 +634,7 @@
 	 *
 	 * @return {boolean|void} Executes a post request that refreshes the page
 	 *                        when the action is performed.
-	 *                        Returns false if a invalid action is given,
+	 *                        Returns false if an invalid action is given,
 	 *                        or when the action cannot be performed.
 	 */
 	action : function(postid, nonce, action) {
@@ -636,7 +835,7 @@
 				btn.removeClass( 'button-activated' );
 				spin.removeClass( 'is-active' );
 			} );
-			// Initialise the Image Editor now that everything is ready.
+			// Initialize the Image Editor now that everything is ready.
 			imageEdit.init( postid );
 		} );
 
@@ -689,7 +888,7 @@
 				elementToSetFocusTo = $( '.imgedit-wrap' ).find( ':tabbable:first' );
 			}
 
-			elementToSetFocusTo.trigger( 'focus' );
+			elementToSetFocusTo.attr( 'tabindex', '-1' ).trigger( 'focus' );
 		}, 100 );
 	},
 
@@ -710,6 +909,8 @@
 		var t = this,
 			selW = $('#imgedit-sel-width-' + postid),
 			selH = $('#imgedit-sel-height-' + postid),
+			selX = $('#imgedit-start-x-' + postid),
+			selY = $('#imgedit-start-y-' + postid),
 			$image = $( image ),
 			$img;
 
@@ -768,6 +969,8 @@
 			 */
 			onSelectStart: function() {
 				imageEdit.setDisabled($('#imgedit-crop-sel-' + postid), 1);
+				imageEdit.setDisabled($('.imgedit-crop-clear'), 1);
+				imageEdit.setDisabled($('.imgedit-crop-apply'), 1);
 			},
 			/**
 			 * Event triggered when the selection is ended.
@@ -781,6 +984,9 @@
 			 */
 			onSelectEnd: function(img, c) {
 				imageEdit.setCropSelection(postid, c);
+				if ( ! $('#imgedit-crop > *').is(':visible') ) {
+					imageEdit.toggleControls($('.imgedit-crop.button'));
+				}
 			},
 
 			/**
@@ -797,6 +1003,8 @@
 				var sizer = imageEdit.hold.sizer;
 				selW.val( imageEdit.round(c.width / sizer) );
 				selH.val( imageEdit.round(c.height / sizer) );
+				selX.val( imageEdit.round(c.x1 / sizer) );
+				selY.val( imageEdit.round(c.y1 / sizer) );
 			}
 		});
 	},
@@ -823,6 +1031,8 @@
 			this.setDisabled( $( '#imgedit-crop-sel-' + postid ), 1 );
 			$('#imgedit-sel-width-' + postid).val('');
 			$('#imgedit-sel-height-' + postid).val('');
+			$('#imgedit-start-x-' + postid).val('0');
+			$('#imgedit-start-y-' + postid).val('0');
 			$('#imgedit-selection-' + postid).val('');
 			return false;
 		}
@@ -953,7 +1163,7 @@
 		if ( $(t).hasClass('disabled') ) {
 			return false;
 		}
-
+		this.closePopup(t);
 		this.addStep({ 'r': { 'r': angle, 'fw': this.hold.h, 'fh': this.hold.w }}, postid, nonce);
 	},
 
@@ -975,7 +1185,7 @@
 		if ( $(t).hasClass('disabled') ) {
 			return false;
 		}
-
+		this.closePopup(t);
 		this.addStep({ 'f': { 'f': axis, 'fw': this.hold.w, 'fh': this.hold.h }}, postid, nonce);
 	},
 
@@ -1011,6 +1221,8 @@
 		// Clear the selection fields after cropping.
 		$('#imgedit-sel-width-' + postid).val('');
 		$('#imgedit-sel-height-' + postid).val('');
+		$('#imgedit-start-x-' + postid).val('0');
+		$('#imgedit-start-y-' + postid).val('0');
 	},
 
 	/**
@@ -1094,6 +1306,8 @@
 	 */
 	setNumSelection : function( postid, el ) {
 		var sel, elX = $('#imgedit-sel-width-' + postid), elY = $('#imgedit-sel-height-' + postid),
+			elX1 = $('#imgedit-start-x-' + postid), elY1 = $('#imgedit-start-y-' + postid),
+			xS = this.intval( elX1.val() ), yS = this.intval( elY1.val() ),
 			x = this.intval( elX.val() ), y = this.intval( elY.val() ),
 			img = $('#image-preview-' + postid), imgh = img.height(), imgw = img.width(),
 			sizer = this.hold.sizer, x1, y1, x2, y2, ias = this.iasapi;
@@ -1112,11 +1326,11 @@
 			return false;
 		}
 
-		if ( x && y && ( sel = ias.getSelection() ) ) {
+		if ( ( ( x && y ) || ( xS && yS ) ) && ( sel = ias.getSelection() ) ) {
 			x2 = sel.x1 + Math.round( x * sizer );
 			y2 = sel.y1 + Math.round( y * sizer );
-			x1 = sel.x1;
-			y1 = sel.y1;
+			x1 = ( xS === sel.x1 ) ? sel.x1 : Math.round( xS * sizer );
+			y1 = ( yS === sel.y1 ) ? sel.y1 : Math.round( yS * sizer );
 
 			if ( x2 > imgw ) {
 				x1 = 0;
@@ -1202,10 +1416,21 @@
 
 				if ( r > h ) {
 					r = h;
+					var errorMessage = __( 'Selected crop ratio exceeds the boundaries of the image. Try a different ratio.' );
+
+					$( '#imgedit-crop-' + postid )
+						.prepend( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' );
+
+					wp.a11y.speak( errorMessage, 'assertive' );
 					if ( n ) {
-						$('#imgedit-crop-height-' + postid).val('');
+						$('#imgedit-crop-height-' + postid).val( '' );
 					} else {
-						$('#imgedit-crop-width-' + postid).val('');
+						$('#imgedit-crop-width-' + postid).val( '');
+					}
+				} else {
+					var error = $( '#imgedit-crop-' + postid ).find( '.notice-error' );
+					if ( 'undefined' !== typeof( error ) ) {
+						error.remove();
 					}
 				}
 
@@ -1228,7 +1453,7 @@
 	 *                        void when it is.
 	 */
 	validateNumeric: function( el ) {
-		if ( ! this.intval( $( el ).val() ) ) {
+		if ( false === this.intval( $( el ).val() ) ) {
 			$( el ).val( '' );
 			return false;
 		}