* The functions necessary for editing images.
* @output wp-admin/js/image-edit.js
/* global ajaxurl, confirm */
* Contains all the methods to initialize and control the image editor.
var imageEdit = window.imageEdit = {
toggleCropTool: function( postid, nonce, cropButton ) {
var img = $( '#image-preview-' + postid ),
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);
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.setSelection( startX, startY, width, height, true );
this.iasapi.setOptions( { show: true } );
* 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('');
// Otherwise, perform the crop.
imageEdit.crop( postid, nonce , cropButton );
* Converts a value to an integer.
* @param {number} f The float value that should be converted.
* @return {number} The integer representation from the float value.
* Bitwise OR operator: one of the obscure ways to truncate floating point figures,
* worth reminding JavaScript doesn't have a distinct "integer" type.
* Adds the disabled attribute and class to a single form element or a field set.
* @param {jQuery} el The element that should be modified.
* @param {boolean|number} s The state for the element. If set to true
* the element is disabled,
* otherwise the element is enabled.
* The function is sometimes called with a 0 or 1
* instead of true or false.
setDisabled : function( el, s ) {
* `el` can be a single form element or a fieldset. Before #28864, the disabled state on
* some text fields was handled targeting $('input', el). Now we need to handle the
* disabled state on buttons too so we can just target `el` regardless if it's a single
* element or a fieldset because when a fieldset is disabled, its descendants are disabled too.
el.removeClass( 'disabled' ).prop( 'disabled', false );
el.addClass( 'disabled' ).prop( 'disabled', true );
* Initializes the image editor.
* @param {number} postid The post ID.
init : function(postid) {
var t = this, old = $('#image-editor-' + t.postid);
if ( t.postid !== postid && old.length ) {
t.hold.sizer = parseFloat( $('#imgedit-sizer-' + postid).val() );
$('#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) {
// Key codes 37 through 40 are the arrow keys.
if ( 36 < k && k < 41 ) {
$(this).trigger( 'blur' );
// The key code 13 is the Enter key.
$( document ).on( 'image-editor-ui-ready', this.focusManager );
* Calculate the image size and save it to memory.
* @param {number} postid The post ID.
calculateImgSize: function( postid ) {
x = t.intval( $( '#imgedit-x-' + postid ).val() ),
y = t.intval( $( '#imgedit-y-' + postid ).val() );
t.hold.w = t.hold.ow = x;
t.hold.h = t.hold.oh = y;
t.hold.sizer = parseFloat( $( '#imgedit-sizer-' + postid ).val() );
t.currentCropSelection = null;
* Toggles the wait/load icon in the editor.
* @since 5.5.0 Added the triggerUIReady parameter.
* @param {number} postid The post ID.
* @param {number} toggle Is 0 or 1, fades the icon in when 1 and out when 0.
* @param {boolean} triggerUIReady Whether to trigger a custom event when the UI is ready. Default false.
toggleEditor: function( postid, toggle, triggerUIReady ) {
var wait = $('#imgedit-wait-' + postid);
wait.fadeOut( 'fast', function() {
$( document ).trigger( 'image-editor-ui-ready' );
* Shows or hides image menu popup.
* @param {HTMLElement} el The activated control element.
* @return {boolean} Always returns false.
togglePopup : function(el) {
var $targetEl = $( el ).attr( 'aria-controls' );
var $target = $( '#' + $targetEl );
.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.
.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' );
* Observes whether the popup should remain open based on focus position.
* @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' );
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 );
* Navigate popup menu by arrow keys.
* @since 6.7.0 Added the event parameter.
* @param {Event} event The key or click event.
* @param {HTMLElement} el The current element.
* @return {boolean} Always returns false.
browsePopup : function(event, el) {
var $collection = $( el ).parent( '.imgedit-popup-menu' ).find( 'button' );
var $index = $collection.index( $el );
var $last = $collection.length;
if ( event.keyCode === 40 ) {
target = $collection.get( $next );
} else if ( event.keyCode === 38 ) {
target = $collection.get( $prev );
* Close popup menu and reset focus on feature activation.
* @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 + '"]' );
.attr( 'aria-expanded', 'false' ).trigger( 'focus' );
.toggleClass( 'imgedit-popup-menu-open' ).slideToggle( 'fast' );
* Shows or hides the image edit help box.
* @param {HTMLElement} el The element to create the help window in.
* @return {boolean} Always returns false.
toggleHelp : function(el) {
.attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' )
.parents( '.imgedit-group-top' ).toggleClass( 'imgedit-help-toggled' ).find( '.imgedit-help' ).slideToggle( 'fast' );
* Shows or hides image edit input fields when enabled.
* @param {HTMLElement} el The element to trigger the edit panel.
* @return {boolean} Always returns false.
toggleControls : function(el) {
var $target = $( '#' + $el.attr( 'aria-controls' ) );
.attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' );
.parent( '.imgedit-group' ).toggleClass( 'imgedit-panel-active' );
* Gets the value from the image edit target.
* The image edit target contains the image sizes where the (possible) changes
* @param {number} postid The post ID.
* @return {string} The value from the imagedit-save-target input field when available,
* 'full' when not selected, or 'all' if it doesn't exist.
getTarget : function( postid ) {
var element = $( '#imgedit-save-target-' + postid );
return element.find( 'input[name="imgedit-target-' + postid + '"]:checked' ).val() || 'full';
* Recalculates the height or width and keeps the original aspect ratio.
* If the original image size is exceeded a red exclamation mark is shown.
* @param {number} postid The current post ID.
* @param {number} x Is 0 when it applies the y-axis
* and 1 when applicable for the x-axis.
* @param {jQuery} el Element.
scaleChanged : function( postid, x, el ) {
var w = $('#imgedit-scale-width-' + postid), h = $('#imgedit-scale-height-' + postid),
warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = '',
scaleBtn = $('#imgedit-scale-button');
if ( false === this.validateNumeric( el ) ) {
h1 = ( w.val() !== '' ) ? Math.round( w.val() / this.hold.xy_ratio ) : '';
w1 = ( h.val() !== '' ) ? Math.round( h.val() * this.hold.xy_ratio ) : '';
if ( ( h1 && h1 > this.hold.oh ) || ( w1 && w1 > this.hold.ow ) ) {
warn.css('visibility', 'visible');
scaleBtn.prop('disabled', true);
warn.css('visibility', 'hidden');
scaleBtn.prop('disabled', false);
* Gets the selected aspect ratio.
* @param {number} postid The post ID.
* @return {string} The aspect ratio.
getSelRatio : function(postid) {
var x = this.hold.w, y = this.hold.h,
X = this.intval( $('#imgedit-crop-width-' + postid).val() ),
Y = this.intval( $('#imgedit-crop-height-' + postid).val() );
* Removes the last action from the image edit history.
* The history consist of (edit) actions performed on the image.