* The functions necessary for editing images.
* @output wp-admin/js/image-edit.js
/* global ajaxurl, confirm */
* Contains all the methods to initialise and control the image editor.
var imageEdit = window.imageEdit = {
* Handle crop tool clicks.
handleCropToolClick: 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();
// 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 } );
// 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),
x = t.intval( $('#imgedit-x-' + postid).val() ),
y = t.intval( $('#imgedit-y-' + postid).val() );
if ( t.postid !== postid && old.length ) {
t.hold.w = t.hold.ow = x;
t.hold.h = t.hold.oh = y;
t.hold.sizer = parseFloat( $('#imgedit-sizer-' + postid).val() );
$('#imgedit-response-' + postid).empty();
$('#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 );
* 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 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' );
* 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,
* or 'full' when not available.
getTarget : function(postid) {
return $('input[name="imgedit-target-' + postid + '"]:checked', '#imgedit-save-target-' + postid).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 = '';
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');
warn.css('visibility', 'hidden');
* 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.
* @param {number} postid The post ID.
* @param {number} setSize 0 or 1, when 1 the image resets to its original size.
* @return {string} JSON string containing the history or an empty string if no history exists.
filterHistory : function(postid, setSize) {
// Apply undo state to history.
var history = $('#imgedit-history-' + postid).val(), pop, n, o, i, op = [];
// Read the JSON string with the image edit history.
history = JSON.parse(history);
pop = this.intval( $('#imgedit-undone-' + postid).val() );
// Reset size to its original state.
this.hold.w = this.hold.ow;
this.hold.h = this.hold.oh;
o = history[history.length - 1];
// c = 'crop', r = 'rotate', f = 'flip'.
o = o.c || o.r || o.f || false;
// fw = Full image width.
// fh = Full image height.
// Filter the last step/action from the history.
if ( i.hasOwnProperty('c') ) {
op[n] = { 'c': { 'x': i.c.x, 'y': i.c.y, 'w': i.c.w, 'h': i.c.h } };
} else if ( i.hasOwnProperty('r') ) {
} else if ( i.hasOwnProperty('f') ) {
return JSON.stringify(op);
* Binds the necessary events to the image.
* When the image source is reloaded the image will be reloaded.
* @param {number} postid The post ID.
* @param {string} nonce The nonce to verify the request.
* @param {function} callback Function to execute when the image is loaded.
refreshEditor : function(postid, nonce, callback) {
t.toggleEditor(postid, 1);
'action': 'imgedit-preview',
'history': t.filterHistory(postid, 1),
'rand': t.intval(Math.random() * 1000000)
img = $( '<img id="image-preview-' + postid + '" alt="" />' )
.on( 'load', { history: data.history }, function( event ) {
parent = $( '#imgedit-crop-' + postid ),
// Checks if there already is some image-edit history.
if ( '' !== event.data.history ) {
historyObj = JSON.parse( event.data.history );
// If last executed action in history is a crop action.
if ( historyObj[historyObj.length - 1].hasOwnProperty( 'c' ) ) {
* A crop action has completed and the crop button gets disabled
* ensure the undo button is enabled.
t.setDisabled( $( '#image-undo-' + postid) , true );
// Move focus to the undo button to avoid a focus loss.
$( '#image-undo-' + postid ).trigger( 'focus' );
parent.empty().append(img);
// w, h are the new full size dimensions.
max1 = Math.max( t.hold.w, t.hold.h );
max2 = Math.max( $(img).width(), $(img).height() );
t.hold.sizer = max1 > max2 ? max2 / max1 : 1;
t.initCrop(postid, img, parent);
if ( (typeof callback !== 'undefined') && callback !== null ) {
if ( $('#imgedit-history-' + postid).val() && $('#imgedit-undone-' + postid).val() === '0' ) {
$('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false);
$('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', true);
t.toggleEditor(postid, 0);
.on( 'error', function() {
var errorMessage = __( 'Could not load the preview image. Please reload the page and try again.' );
$( '#imgedit-crop-' + postid )
.append( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' );
t.toggleEditor( postid, 0, true );
wp.a11y.speak( errorMessage, 'assertive' );
.attr('src', ajaxurl + '?' + $.param(data));
* Performs an image edit action.
* @param {number} postid The post ID.
* @param {string} nonce The nonce to verify the request.
* @param {string} action The action to perform on the image.
* The possible actions are: "scale" and "restore".
* @return {boolean|void} Executes a post request that refreshes the page
* when the action is performed.
* Returns false if a invalid action is given,
* or when the action cannot be performed.
action : function(postid, nonce, action) {
var t = this, data, w, h, fw, fh;
if ( t.notsaved(postid) ) {
'action': 'image-editor',
if ( 'scale' === action ) {
w = $('#imgedit-scale-width-' + postid),
h = $('#imgedit-scale-height-' + postid),
if ( fw === t.hold.ow || fh === t.hold.oh ) {
} else if ( 'restore' === action ) {
t.toggleEditor(postid, 1);
$.post( ajaxurl, data, function( response ) {
$( '#image-editor-' + postid ).empty().append( response.data.html );
t.toggleEditor( postid, 0, true );
// Refresh the attachment model so that changes propagate.
} ).done( function( response ) {
// Whether the executed action was `scale` or `restore`, the response does have a message.
if ( response && response.data.message.msg ) {
wp.a11y.speak( response.data.message.msg );
if ( response && response.data.message.error ) {
wp.a11y.speak( response.data.message.error );