diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-admin/js/image-edit.js --- 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( '' ); + + 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; }