: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
// 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' ) {
$('button.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false);
$('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.' );
$( '#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 an 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 );
* Stores the changes that are made to the image.
* @param {number} postid The post ID to get the image from the database.
* @param {string} nonce The nonce to verify the request.
* @return {boolean|void} If the actions are successfully saved a response message is shown.
* Returns false if there is no image editing history,
* thus there are not edit-actions performed on the image.
save : function(postid, nonce) {
target = this.getTarget(postid),
history = this.filterHistory(postid, 0),
this.toggleEditor(postid, 1);
'action': 'image-editor',
'context': $('#image-edit-context').length ? $('#image-edit-context').val() : null,
// Post the image edit data to the backend.
$.post( ajaxurl, data, function( response ) {
// If a response is returned, close the editor and show an error.
if ( response.data.error ) {
$( '#imgedit-response-' + postid )
.html( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + response.data.error + '</p></div>' );
wp.a11y.speak( response.data.error );
if ( response.data.fw && response.data.fh ) {
$( '#media-dims-' + postid ).html( response.data.fw + ' × ' + response.data.fh );
if ( response.data.thumbnail ) {
$( '.thumbnail', '#thumbnail-head-' + postid ).attr( 'src', '' + response.data.thumbnail );
if ( response.data.msg ) {
$( '#imgedit-response-' + postid )
.html( '<div class="notice notice-success" tabindex="-1" role="alert"><p>' + response.data.msg + '</p></div>' );
wp.a11y.speak( response.data.msg );
* Creates the image edit window.
* @param {number} postid The post ID for the image.
* @param {string} nonce The nonce to verify the request.
* @param {Object} view The image editor view to be used for the editing.
* @return {void|promise} Either returns void if the button was already activated
* or returns an instance of the image editor, wrapped in a promise.
open : function( postid, nonce, view ) {
elem = $( '#image-editor-' + postid ),
head = $( '#media-head-' + postid ),
btn = $( '#imgedit-open-btn-' + postid ),
spin = btn.siblings( '.spinner' );
* Instead of disabling the button, which causes a focus loss and makes screen
* readers announce "unavailable", return if the button was already clicked.
if ( btn.hasClass( 'button-activated' ) ) {
spin.addClass( 'is-active' );
'action': 'image-editor',
btn.addClass( 'button-activated' );
} ).done( function( response ) {
if ( '-1' === response ) {
errorMessage = __( 'Could not load the preview image.' );
elem.html( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' );
if ( response.data && response.data.html ) {
elem.html( response.data.html );
head.fadeOut( 'fast', function() {
elem.fadeIn( 'fast', function() {
$( document ).trigger( 'image-editor-ui-ready' );
btn.removeClass( 'button-activated' );
spin.removeClass( 'is-active' );
// Initialize the Image Editor now that everything is ready.
imageEdit.init( postid );
* Initializes the cropping tool and sets a default cropping selection.
* @param {number} postid The post ID.
imgLoaded : function(postid) {
var img = $('#image-preview-' + postid), parent = $('#imgedit-crop-' + postid);
// Ensure init has run even when directly loaded.
if ( 'undefined' === typeof this.hold.sizer ) {
this.initCrop(postid, img, parent);
this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0, 'width': img.innerWidth(), 'height': img.innerHeight() } );
this.toggleEditor( postid, 0, true );
* Manages keyboard focus in the Image Editor user interface.
focusManager: function() {
* Editor is ready. Move focus to one of the admin alert notices displayed
* after a user action or to the first focusable element. Since the DOM
* update is pretty large, the timeout helps browsers update their
* accessibility tree to better support assistive technologies.
var elementToSetFocusTo = $( '.notice[role="alert"]' );
if ( ! elementToSetFocusTo.length ) {
elementToSetFocusTo = $( '.imgedit-wrap' ).find( ':tabbable:first' );
elementToSetFocusTo.attr( 'tabindex', '-1' ).trigger( 'focus' );
* Initializes the cropping tool.
* @param {number} postid The post ID.
* @param {HTMLElement} image The preview image.
* @param {HTMLElement} parent The preview image container.
initCrop : function(postid, image, parent) {
selW = $('#imgedit-sel-width-' + postid),
selH = $('#imgedit-sel-height-' + postid),
selX = $('#imgedit-start-x-' + postid),
selY = $('#imgedit-start-y-' + postid),
if ( $image.data( 'imgAreaSelect' ) ) {
t.iasapi = $image.imgAreaSelect({
* Sets the CSS styles and binds events for locking the aspect ratio.
* @param {jQuery} img The preview image.
onInit: function( img ) {
// Ensure that the imgAreaSelect wrapper elements are position:absolute
// (even if we're in a position:fixed modal).
$img.next().css( 'position', 'absolute' )
.nextAll( '.imgareaselect-outer' ).css( 'position', 'absolute' );
* Binds mouse down event to the cropping container.
parent.children().on( 'mousedown, touchstart', function(e){
var ratio = false, sel, defRatio;
sel = t.iasapi.getSelection();
defRatio = t.getSelRatio(postid);
ratio = ( sel && sel.width && sel.height ) ? sel.width + ':' + sel.height : defRatio;
* Event triggered when starting a selection.
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.
* @param {Object} img jQuery object representing the image.
* @param {Object} c The selection.
onSelectEnd: function(img, c) {
imageEdit.setCropSelection(postid, c);
if ( ! $('#imgedit-crop > *').is(':visible') ) {
imageEdit.toggleControls($('.imgedit-crop.button'));
* Event triggered when the selection changes.
* @param {Object} img jQuery object representing the image.
* @param {Object} c The selection.