ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

image-edit.js (31958B)


      1 /**
      2  * The functions necessary for editing images.
      3  *
      4  * @since 2.9.0
      5  * @output wp-admin/js/image-edit.js
      6  */
      7 
      8  /* global ajaxurl, confirm */
      9 
     10 (function($) {
     11 	var __ = wp.i18n.__;
     12 
     13 	/**
     14 	 * Contains all the methods to initialise and control the image editor.
     15 	 *
     16 	 * @namespace imageEdit
     17 	 */
     18 	var imageEdit = window.imageEdit = {
     19 	iasapi : {},
     20 	hold : {},
     21 	postid : '',
     22 	_view : false,
     23 
     24 	/**
     25 	 * Handle crop tool clicks.
     26 	 */
     27 	handleCropToolClick: function( postid, nonce, cropButton ) {
     28 		var img = $( '#image-preview-' + postid ),
     29 			selection = this.iasapi.getSelection();
     30 
     31 		// Ensure selection is available, otherwise reset to full image.
     32 		if ( isNaN( selection.x1 ) ) {
     33 			this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': img.innerWidth(), 'y2': img.innerHeight(), 'width': img.innerWidth(), 'height': img.innerHeight() } );
     34 			selection = this.iasapi.getSelection();
     35 		}
     36 
     37 		// If we don't already have a selection, select the entire image.
     38 		if ( 0 === selection.x1 && 0 === selection.y1 && 0 === selection.x2 && 0 === selection.y2 ) {
     39 			this.iasapi.setSelection( 0, 0, img.innerWidth(), img.innerHeight(), true );
     40 			this.iasapi.setOptions( { show: true } );
     41 			this.iasapi.update();
     42 		} else {
     43 
     44 			// Otherwise, perform the crop.
     45 			imageEdit.crop( postid, nonce , cropButton );
     46 		}
     47 	},
     48 
     49 	/**
     50 	 * Converts a value to an integer.
     51 	 *
     52 	 * @since 2.9.0
     53 	 *
     54 	 * @memberof imageEdit
     55 	 *
     56 	 * @param {number} f The float value that should be converted.
     57 	 *
     58 	 * @return {number} The integer representation from the float value.
     59 	 */
     60 	intval : function(f) {
     61 		/*
     62 		 * Bitwise OR operator: one of the obscure ways to truncate floating point figures,
     63 		 * worth reminding JavaScript doesn't have a distinct "integer" type.
     64 		 */
     65 		return f | 0;
     66 	},
     67 
     68 	/**
     69 	 * Adds the disabled attribute and class to a single form element or a field set.
     70 	 *
     71 	 * @since 2.9.0
     72 	 *
     73 	 * @memberof imageEdit
     74 	 *
     75 	 * @param {jQuery}         el The element that should be modified.
     76 	 * @param {boolean|number} s  The state for the element. If set to true
     77 	 *                            the element is disabled,
     78 	 *                            otherwise the element is enabled.
     79 	 *                            The function is sometimes called with a 0 or 1
     80 	 *                            instead of true or false.
     81 	 *
     82 	 * @return {void}
     83 	 */
     84 	setDisabled : function( el, s ) {
     85 		/*
     86 		 * `el` can be a single form element or a fieldset. Before #28864, the disabled state on
     87 		 * some text fields  was handled targeting $('input', el). Now we need to handle the
     88 		 * disabled state on buttons too so we can just target `el` regardless if it's a single
     89 		 * element or a fieldset because when a fieldset is disabled, its descendants are disabled too.
     90 		 */
     91 		if ( s ) {
     92 			el.removeClass( 'disabled' ).prop( 'disabled', false );
     93 		} else {
     94 			el.addClass( 'disabled' ).prop( 'disabled', true );
     95 		}
     96 	},
     97 
     98 	/**
     99 	 * Initializes the image editor.
    100 	 *
    101 	 * @since 2.9.0
    102 	 *
    103 	 * @memberof imageEdit
    104 	 *
    105 	 * @param {number} postid The post ID.
    106 	 *
    107 	 * @return {void}
    108 	 */
    109 	init : function(postid) {
    110 		var t = this, old = $('#image-editor-' + t.postid),
    111 			x = t.intval( $('#imgedit-x-' + postid).val() ),
    112 			y = t.intval( $('#imgedit-y-' + postid).val() );
    113 
    114 		if ( t.postid !== postid && old.length ) {
    115 			t.close(t.postid);
    116 		}
    117 
    118 		t.hold.w = t.hold.ow = x;
    119 		t.hold.h = t.hold.oh = y;
    120 		t.hold.xy_ratio = x / y;
    121 		t.hold.sizer = parseFloat( $('#imgedit-sizer-' + postid).val() );
    122 		t.postid = postid;
    123 		$('#imgedit-response-' + postid).empty();
    124 
    125 		$('#imgedit-panel-' + postid).on( 'keypress', 'input[type="text"]', function(e) {
    126 			var k = e.keyCode;
    127 
    128 			// Key codes 37 through 40 are the arrow keys.
    129 			if ( 36 < k && k < 41 ) {
    130 				$(this).trigger( 'blur' );
    131 			}
    132 
    133 			// The key code 13 is the Enter key.
    134 			if ( 13 === k ) {
    135 				e.preventDefault();
    136 				e.stopPropagation();
    137 				return false;
    138 			}
    139 		});
    140 
    141 		$( document ).on( 'image-editor-ui-ready', this.focusManager );
    142 	},
    143 
    144 	/**
    145 	 * Toggles the wait/load icon in the editor.
    146 	 *
    147 	 * @since 2.9.0
    148 	 * @since 5.5.0 Added the triggerUIReady parameter.
    149 	 *
    150 	 * @memberof imageEdit
    151 	 *
    152 	 * @param {number}  postid         The post ID.
    153 	 * @param {number}  toggle         Is 0 or 1, fades the icon in when 1 and out when 0.
    154 	 * @param {boolean} triggerUIReady Whether to trigger a custom event when the UI is ready. Default false.
    155 	 *
    156 	 * @return {void}
    157 	 */
    158 	toggleEditor: function( postid, toggle, triggerUIReady ) {
    159 		var wait = $('#imgedit-wait-' + postid);
    160 
    161 		if ( toggle ) {
    162 			wait.fadeIn( 'fast' );
    163 		} else {
    164 			wait.fadeOut( 'fast', function() {
    165 				if ( triggerUIReady ) {
    166 					$( document ).trigger( 'image-editor-ui-ready' );
    167 				}
    168 			} );
    169 		}
    170 	},
    171 
    172 	/**
    173 	 * Shows or hides the image edit help box.
    174 	 *
    175 	 * @since 2.9.0
    176 	 *
    177 	 * @memberof imageEdit
    178 	 *
    179 	 * @param {HTMLElement} el The element to create the help window in.
    180 	 *
    181 	 * @return {boolean} Always returns false.
    182 	 */
    183 	toggleHelp : function(el) {
    184 		var $el = $( el );
    185 		$el
    186 			.attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' )
    187 			.parents( '.imgedit-group-top' ).toggleClass( 'imgedit-help-toggled' ).find( '.imgedit-help' ).slideToggle( 'fast' );
    188 
    189 		return false;
    190 	},
    191 
    192 	/**
    193 	 * Gets the value from the image edit target.
    194 	 *
    195 	 * The image edit target contains the image sizes where the (possible) changes
    196 	 * have to be applied to.
    197 	 *
    198 	 * @since 2.9.0
    199 	 *
    200 	 * @memberof imageEdit
    201 	 *
    202 	 * @param {number} postid The post ID.
    203 	 *
    204 	 * @return {string} The value from the imagedit-save-target input field when available,
    205 	 *                  or 'full' when not available.
    206 	 */
    207 	getTarget : function(postid) {
    208 		return $('input[name="imgedit-target-' + postid + '"]:checked', '#imgedit-save-target-' + postid).val() || 'full';
    209 	},
    210 
    211 	/**
    212 	 * Recalculates the height or width and keeps the original aspect ratio.
    213 	 *
    214 	 * If the original image size is exceeded a red exclamation mark is shown.
    215 	 *
    216 	 * @since 2.9.0
    217 	 *
    218 	 * @memberof imageEdit
    219 	 *
    220 	 * @param {number}         postid The current post ID.
    221 	 * @param {number}         x      Is 0 when it applies the y-axis
    222 	 *                                and 1 when applicable for the x-axis.
    223 	 * @param {jQuery}         el     Element.
    224 	 *
    225 	 * @return {void}
    226 	 */
    227 	scaleChanged : function( postid, x, el ) {
    228 		var w = $('#imgedit-scale-width-' + postid), h = $('#imgedit-scale-height-' + postid),
    229 		warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = '';
    230 
    231 		if ( false === this.validateNumeric( el ) ) {
    232 			return;
    233 		}
    234 
    235 		if ( x ) {
    236 			h1 = ( w.val() !== '' ) ? Math.round( w.val() / this.hold.xy_ratio ) : '';
    237 			h.val( h1 );
    238 		} else {
    239 			w1 = ( h.val() !== '' ) ? Math.round( h.val() * this.hold.xy_ratio ) : '';
    240 			w.val( w1 );
    241 		}
    242 
    243 		if ( ( h1 && h1 > this.hold.oh ) || ( w1 && w1 > this.hold.ow ) ) {
    244 			warn.css('visibility', 'visible');
    245 		} else {
    246 			warn.css('visibility', 'hidden');
    247 		}
    248 	},
    249 
    250 	/**
    251 	 * Gets the selected aspect ratio.
    252 	 *
    253 	 * @since 2.9.0
    254 	 *
    255 	 * @memberof imageEdit
    256 	 *
    257 	 * @param {number} postid The post ID.
    258 	 *
    259 	 * @return {string} The aspect ratio.
    260 	 */
    261 	getSelRatio : function(postid) {
    262 		var x = this.hold.w, y = this.hold.h,
    263 			X = this.intval( $('#imgedit-crop-width-' + postid).val() ),
    264 			Y = this.intval( $('#imgedit-crop-height-' + postid).val() );
    265 
    266 		if ( X && Y ) {
    267 			return X + ':' + Y;
    268 		}
    269 
    270 		if ( x && y ) {
    271 			return x + ':' + y;
    272 		}
    273 
    274 		return '1:1';
    275 	},
    276 
    277 	/**
    278 	 * Removes the last action from the image edit history.
    279 	 * The history consist of (edit) actions performed on the image.
    280 	 *
    281 	 * @since 2.9.0
    282 	 *
    283 	 * @memberof imageEdit
    284 	 *
    285 	 * @param {number} postid  The post ID.
    286 	 * @param {number} setSize 0 or 1, when 1 the image resets to its original size.
    287 	 *
    288 	 * @return {string} JSON string containing the history or an empty string if no history exists.
    289 	 */
    290 	filterHistory : function(postid, setSize) {
    291 		// Apply undo state to history.
    292 		var history = $('#imgedit-history-' + postid).val(), pop, n, o, i, op = [];
    293 
    294 		if ( history !== '' ) {
    295 			// Read the JSON string with the image edit history.
    296 			history = JSON.parse(history);
    297 			pop = this.intval( $('#imgedit-undone-' + postid).val() );
    298 			if ( pop > 0 ) {
    299 				while ( pop > 0 ) {
    300 					history.pop();
    301 					pop--;
    302 				}
    303 			}
    304 
    305 			// Reset size to its original state.
    306 			if ( setSize ) {
    307 				if ( !history.length ) {
    308 					this.hold.w = this.hold.ow;
    309 					this.hold.h = this.hold.oh;
    310 					return '';
    311 				}
    312 
    313 				// Restore original 'o'.
    314 				o = history[history.length - 1];
    315 
    316 				// c = 'crop', r = 'rotate', f = 'flip'.
    317 				o = o.c || o.r || o.f || false;
    318 
    319 				if ( o ) {
    320 					// fw = Full image width.
    321 					this.hold.w = o.fw;
    322 					// fh = Full image height.
    323 					this.hold.h = o.fh;
    324 				}
    325 			}
    326 
    327 			// Filter the last step/action from the history.
    328 			for ( n in history ) {
    329 				i = history[n];
    330 				if ( i.hasOwnProperty('c') ) {
    331 					op[n] = { 'c': { 'x': i.c.x, 'y': i.c.y, 'w': i.c.w, 'h': i.c.h } };
    332 				} else if ( i.hasOwnProperty('r') ) {
    333 					op[n] = { 'r': i.r.r };
    334 				} else if ( i.hasOwnProperty('f') ) {
    335 					op[n] = { 'f': i.f.f };
    336 				}
    337 			}
    338 			return JSON.stringify(op);
    339 		}
    340 		return '';
    341 	},
    342 	/**
    343 	 * Binds the necessary events to the image.
    344 	 *
    345 	 * When the image source is reloaded the image will be reloaded.
    346 	 *
    347 	 * @since 2.9.0
    348 	 *
    349 	 * @memberof imageEdit
    350 	 *
    351 	 * @param {number}   postid   The post ID.
    352 	 * @param {string}   nonce    The nonce to verify the request.
    353 	 * @param {function} callback Function to execute when the image is loaded.
    354 	 *
    355 	 * @return {void}
    356 	 */
    357 	refreshEditor : function(postid, nonce, callback) {
    358 		var t = this, data, img;
    359 
    360 		t.toggleEditor(postid, 1);
    361 		data = {
    362 			'action': 'imgedit-preview',
    363 			'_ajax_nonce': nonce,
    364 			'postid': postid,
    365 			'history': t.filterHistory(postid, 1),
    366 			'rand': t.intval(Math.random() * 1000000)
    367 		};
    368 
    369 		img = $( '<img id="image-preview-' + postid + '" alt="" />' )
    370 			.on( 'load', { history: data.history }, function( event ) {
    371 				var max1, max2,
    372 					parent = $( '#imgedit-crop-' + postid ),
    373 					t = imageEdit,
    374 					historyObj;
    375 
    376 				// Checks if there already is some image-edit history.
    377 				if ( '' !== event.data.history ) {
    378 					historyObj = JSON.parse( event.data.history );
    379 					// If last executed action in history is a crop action.
    380 					if ( historyObj[historyObj.length - 1].hasOwnProperty( 'c' ) ) {
    381 						/*
    382 						 * A crop action has completed and the crop button gets disabled
    383 						 * ensure the undo button is enabled.
    384 						 */
    385 						t.setDisabled( $( '#image-undo-' + postid) , true );
    386 						// Move focus to the undo button to avoid a focus loss.
    387 						$( '#image-undo-' + postid ).trigger( 'focus' );
    388 					}
    389 				}
    390 
    391 				parent.empty().append(img);
    392 
    393 				// w, h are the new full size dimensions.
    394 				max1 = Math.max( t.hold.w, t.hold.h );
    395 				max2 = Math.max( $(img).width(), $(img).height() );
    396 				t.hold.sizer = max1 > max2 ? max2 / max1 : 1;
    397 
    398 				t.initCrop(postid, img, parent);
    399 
    400 				if ( (typeof callback !== 'undefined') && callback !== null ) {
    401 					callback();
    402 				}
    403 
    404 				if ( $('#imgedit-history-' + postid).val() && $('#imgedit-undone-' + postid).val() === '0' ) {
    405 					$('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false);
    406 				} else {
    407 					$('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', true);
    408 				}
    409 
    410 				t.toggleEditor(postid, 0);
    411 			})
    412 			.on( 'error', function() {
    413 				var errorMessage = __( 'Could not load the preview image. Please reload the page and try again.' );
    414 
    415 				$( '#imgedit-crop-' + postid )
    416 					.empty()
    417 					.append( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' );
    418 
    419 				t.toggleEditor( postid, 0, true );
    420 				wp.a11y.speak( errorMessage, 'assertive' );
    421 			} )
    422 			.attr('src', ajaxurl + '?' + $.param(data));
    423 	},
    424 	/**
    425 	 * Performs an image edit action.
    426 	 *
    427 	 * @since 2.9.0
    428 	 *
    429 	 * @memberof imageEdit
    430 	 *
    431 	 * @param {number} postid The post ID.
    432 	 * @param {string} nonce  The nonce to verify the request.
    433 	 * @param {string} action The action to perform on the image.
    434 	 *                        The possible actions are: "scale" and "restore".
    435 	 *
    436 	 * @return {boolean|void} Executes a post request that refreshes the page
    437 	 *                        when the action is performed.
    438 	 *                        Returns false if a invalid action is given,
    439 	 *                        or when the action cannot be performed.
    440 	 */
    441 	action : function(postid, nonce, action) {
    442 		var t = this, data, w, h, fw, fh;
    443 
    444 		if ( t.notsaved(postid) ) {
    445 			return false;
    446 		}
    447 
    448 		data = {
    449 			'action': 'image-editor',
    450 			'_ajax_nonce': nonce,
    451 			'postid': postid
    452 		};
    453 
    454 		if ( 'scale' === action ) {
    455 			w = $('#imgedit-scale-width-' + postid),
    456 			h = $('#imgedit-scale-height-' + postid),
    457 			fw = t.intval(w.val()),
    458 			fh = t.intval(h.val());
    459 
    460 			if ( fw < 1 ) {
    461 				w.trigger( 'focus' );
    462 				return false;
    463 			} else if ( fh < 1 ) {
    464 				h.trigger( 'focus' );
    465 				return false;
    466 			}
    467 
    468 			if ( fw === t.hold.ow || fh === t.hold.oh ) {
    469 				return false;
    470 			}
    471 
    472 			data['do'] = 'scale';
    473 			data.fwidth = fw;
    474 			data.fheight = fh;
    475 		} else if ( 'restore' === action ) {
    476 			data['do'] = 'restore';
    477 		} else {
    478 			return false;
    479 		}
    480 
    481 		t.toggleEditor(postid, 1);
    482 		$.post( ajaxurl, data, function( response ) {
    483 			$( '#image-editor-' + postid ).empty().append( response.data.html );
    484 			t.toggleEditor( postid, 0, true );
    485 			// Refresh the attachment model so that changes propagate.
    486 			if ( t._view ) {
    487 				t._view.refresh();
    488 			}
    489 		} ).done( function( response ) {
    490 			// Whether the executed action was `scale` or `restore`, the response does have a message.
    491 			if ( response && response.data.message.msg ) {
    492 				wp.a11y.speak( response.data.message.msg );
    493 				return;
    494 			}
    495 
    496 			if ( response && response.data.message.error ) {
    497 				wp.a11y.speak( response.data.message.error );
    498 			}
    499 		} );
    500 	},
    501 
    502 	/**
    503 	 * Stores the changes that are made to the image.
    504 	 *
    505 	 * @since 2.9.0
    506 	 *
    507 	 * @memberof imageEdit
    508 	 *
    509 	 * @param {number}  postid   The post ID to get the image from the database.
    510 	 * @param {string}  nonce    The nonce to verify the request.
    511 	 *
    512 	 * @return {boolean|void}  If the actions are successfully saved a response message is shown.
    513 	 *                         Returns false if there is no image editing history,
    514 	 *                         thus there are not edit-actions performed on the image.
    515 	 */
    516 	save : function(postid, nonce) {
    517 		var data,
    518 			target = this.getTarget(postid),
    519 			history = this.filterHistory(postid, 0),
    520 			self = this;
    521 
    522 		if ( '' === history ) {
    523 			return false;
    524 		}
    525 
    526 		this.toggleEditor(postid, 1);
    527 		data = {
    528 			'action': 'image-editor',
    529 			'_ajax_nonce': nonce,
    530 			'postid': postid,
    531 			'history': history,
    532 			'target': target,
    533 			'context': $('#image-edit-context').length ? $('#image-edit-context').val() : null,
    534 			'do': 'save'
    535 		};
    536 		// Post the image edit data to the backend.
    537 		$.post( ajaxurl, data, function( response ) {
    538 			// If a response is returned, close the editor and show an error.
    539 			if ( response.data.error ) {
    540 				$( '#imgedit-response-' + postid )
    541 					.html( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + response.data.error + '</p></div>' );
    542 
    543 				imageEdit.close(postid);
    544 				wp.a11y.speak( response.data.error );
    545 				return;
    546 			}
    547 
    548 			if ( response.data.fw && response.data.fh ) {
    549 				$( '#media-dims-' + postid ).html( response.data.fw + ' &times; ' + response.data.fh );
    550 			}
    551 
    552 			if ( response.data.thumbnail ) {
    553 				$( '.thumbnail', '#thumbnail-head-' + postid ).attr( 'src', '' + response.data.thumbnail );
    554 			}
    555 
    556 			if ( response.data.msg ) {
    557 				$( '#imgedit-response-' + postid )
    558 					.html( '<div class="notice notice-success" tabindex="-1" role="alert"><p>' + response.data.msg + '</p></div>' );
    559 
    560 				wp.a11y.speak( response.data.msg );
    561 			}
    562 
    563 			if ( self._view ) {
    564 				self._view.save();
    565 			} else {
    566 				imageEdit.close(postid);
    567 			}
    568 		});
    569 	},
    570 
    571 	/**
    572 	 * Creates the image edit window.
    573 	 *
    574 	 * @since 2.9.0
    575 	 *
    576 	 * @memberof imageEdit
    577 	 *
    578 	 * @param {number} postid   The post ID for the image.
    579 	 * @param {string} nonce    The nonce to verify the request.
    580 	 * @param {Object} view     The image editor view to be used for the editing.
    581 	 *
    582 	 * @return {void|promise} Either returns void if the button was already activated
    583 	 *                        or returns an instance of the image editor, wrapped in a promise.
    584 	 */
    585 	open : function( postid, nonce, view ) {
    586 		this._view = view;
    587 
    588 		var dfd, data,
    589 			elem = $( '#image-editor-' + postid ),
    590 			head = $( '#media-head-' + postid ),
    591 			btn = $( '#imgedit-open-btn-' + postid ),
    592 			spin = btn.siblings( '.spinner' );
    593 
    594 		/*
    595 		 * Instead of disabling the button, which causes a focus loss and makes screen
    596 		 * readers announce "unavailable", return if the button was already clicked.
    597 		 */
    598 		if ( btn.hasClass( 'button-activated' ) ) {
    599 			return;
    600 		}
    601 
    602 		spin.addClass( 'is-active' );
    603 
    604 		data = {
    605 			'action': 'image-editor',
    606 			'_ajax_nonce': nonce,
    607 			'postid': postid,
    608 			'do': 'open'
    609 		};
    610 
    611 		dfd = $.ajax( {
    612 			url:  ajaxurl,
    613 			type: 'post',
    614 			data: data,
    615 			beforeSend: function() {
    616 				btn.addClass( 'button-activated' );
    617 			}
    618 		} ).done( function( response ) {
    619 			var errorMessage;
    620 
    621 			if ( '-1' === response ) {
    622 				errorMessage = __( 'Could not load the preview image.' );
    623 				elem.html( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' );
    624 			}
    625 
    626 			if ( response.data && response.data.html ) {
    627 				elem.html( response.data.html );
    628 			}
    629 
    630 			head.fadeOut( 'fast', function() {
    631 				elem.fadeIn( 'fast', function() {
    632 					if ( errorMessage ) {
    633 						$( document ).trigger( 'image-editor-ui-ready' );
    634 					}
    635 				} );
    636 				btn.removeClass( 'button-activated' );
    637 				spin.removeClass( 'is-active' );
    638 			} );
    639 			// Initialise the Image Editor now that everything is ready.
    640 			imageEdit.init( postid );
    641 		} );
    642 
    643 		return dfd;
    644 	},
    645 
    646 	/**
    647 	 * Initializes the cropping tool and sets a default cropping selection.
    648 	 *
    649 	 * @since 2.9.0
    650 	 *
    651 	 * @memberof imageEdit
    652 	 *
    653 	 * @param {number} postid The post ID.
    654 	 *
    655 	 * @return {void}
    656 	 */
    657 	imgLoaded : function(postid) {
    658 		var img = $('#image-preview-' + postid), parent = $('#imgedit-crop-' + postid);
    659 
    660 		// Ensure init has run even when directly loaded.
    661 		if ( 'undefined' === typeof this.hold.sizer ) {
    662 			this.init( postid );
    663 		}
    664 
    665 		this.initCrop(postid, img, parent);
    666 		this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0, 'width': img.innerWidth(), 'height': img.innerHeight() } );
    667 
    668 		this.toggleEditor( postid, 0, true );
    669 	},
    670 
    671 	/**
    672 	 * Manages keyboard focus in the Image Editor user interface.
    673 	 *
    674 	 * @since 5.5.0
    675 	 *
    676 	 * @return {void}
    677 	 */
    678 	focusManager: function() {
    679 		/*
    680 		 * Editor is ready. Move focus to one of the admin alert notices displayed
    681 		 * after a user action or to the first focusable element. Since the DOM
    682 		 * update is pretty large, the timeout helps browsers update their
    683 		 * accessibility tree to better support assistive technologies.
    684 		 */
    685 		setTimeout( function() {
    686 			var elementToSetFocusTo = $( '.notice[role="alert"]' );
    687 
    688 			if ( ! elementToSetFocusTo.length ) {
    689 				elementToSetFocusTo = $( '.imgedit-wrap' ).find( ':tabbable:first' );
    690 			}
    691 
    692 			elementToSetFocusTo.trigger( 'focus' );
    693 		}, 100 );
    694 	},
    695 
    696 	/**
    697 	 * Initializes the cropping tool.
    698 	 *
    699 	 * @since 2.9.0
    700 	 *
    701 	 * @memberof imageEdit
    702 	 *
    703 	 * @param {number}      postid The post ID.
    704 	 * @param {HTMLElement} image  The preview image.
    705 	 * @param {HTMLElement} parent The preview image container.
    706 	 *
    707 	 * @return {void}
    708 	 */
    709 	initCrop : function(postid, image, parent) {
    710 		var t = this,
    711 			selW = $('#imgedit-sel-width-' + postid),
    712 			selH = $('#imgedit-sel-height-' + postid),
    713 			$image = $( image ),
    714 			$img;
    715 
    716 		// Already initialized?
    717 		if ( $image.data( 'imgAreaSelect' ) ) {
    718 			return;
    719 		}
    720 
    721 		t.iasapi = $image.imgAreaSelect({
    722 			parent: parent,
    723 			instance: true,
    724 			handles: true,
    725 			keys: true,
    726 			minWidth: 3,
    727 			minHeight: 3,
    728 
    729 			/**
    730 			 * Sets the CSS styles and binds events for locking the aspect ratio.
    731 			 *
    732 			 * @ignore
    733 			 *
    734 			 * @param {jQuery} img The preview image.
    735 			 */
    736 			onInit: function( img ) {
    737 				// Ensure that the imgAreaSelect wrapper elements are position:absolute
    738 				// (even if we're in a position:fixed modal).
    739 				$img = $( img );
    740 				$img.next().css( 'position', 'absolute' )
    741 					.nextAll( '.imgareaselect-outer' ).css( 'position', 'absolute' );
    742 				/**
    743 				 * Binds mouse down event to the cropping container.
    744 				 *
    745 				 * @return {void}
    746 				 */
    747 				parent.children().on( 'mousedown, touchstart', function(e){
    748 					var ratio = false, sel, defRatio;
    749 
    750 					if ( e.shiftKey ) {
    751 						sel = t.iasapi.getSelection();
    752 						defRatio = t.getSelRatio(postid);
    753 						ratio = ( sel && sel.width && sel.height ) ? sel.width + ':' + sel.height : defRatio;
    754 					}
    755 
    756 					t.iasapi.setOptions({
    757 						aspectRatio: ratio
    758 					});
    759 				});
    760 			},
    761 
    762 			/**
    763 			 * Event triggered when starting a selection.
    764 			 *
    765 			 * @ignore
    766 			 *
    767 			 * @return {void}
    768 			 */
    769 			onSelectStart: function() {
    770 				imageEdit.setDisabled($('#imgedit-crop-sel-' + postid), 1);
    771 			},
    772 			/**
    773 			 * Event triggered when the selection is ended.
    774 			 *
    775 			 * @ignore
    776 			 *
    777 			 * @param {Object} img jQuery object representing the image.
    778 			 * @param {Object} c   The selection.
    779 			 *
    780 			 * @return {Object}
    781 			 */
    782 			onSelectEnd: function(img, c) {
    783 				imageEdit.setCropSelection(postid, c);
    784 			},
    785 
    786 			/**
    787 			 * Event triggered when the selection changes.
    788 			 *
    789 			 * @ignore
    790 			 *
    791 			 * @param {Object} img jQuery object representing the image.
    792 			 * @param {Object} c   The selection.
    793 			 *
    794 			 * @return {void}
    795 			 */
    796 			onSelectChange: function(img, c) {
    797 				var sizer = imageEdit.hold.sizer;
    798 				selW.val( imageEdit.round(c.width / sizer) );
    799 				selH.val( imageEdit.round(c.height / sizer) );
    800 			}
    801 		});
    802 	},
    803 
    804 	/**
    805 	 * Stores the current crop selection.
    806 	 *
    807 	 * @since 2.9.0
    808 	 *
    809 	 * @memberof imageEdit
    810 	 *
    811 	 * @param {number} postid The post ID.
    812 	 * @param {Object} c      The selection.
    813 	 *
    814 	 * @return {boolean}
    815 	 */
    816 	setCropSelection : function(postid, c) {
    817 		var sel;
    818 
    819 		c = c || 0;
    820 
    821 		if ( !c || ( c.width < 3 && c.height < 3 ) ) {
    822 			this.setDisabled( $( '.imgedit-crop', '#imgedit-panel-' + postid ), 1 );
    823 			this.setDisabled( $( '#imgedit-crop-sel-' + postid ), 1 );
    824 			$('#imgedit-sel-width-' + postid).val('');
    825 			$('#imgedit-sel-height-' + postid).val('');
    826 			$('#imgedit-selection-' + postid).val('');
    827 			return false;
    828 		}
    829 
    830 		sel = { 'x': c.x1, 'y': c.y1, 'w': c.width, 'h': c.height };
    831 		this.setDisabled($('.imgedit-crop', '#imgedit-panel-' + postid), 1);
    832 		$('#imgedit-selection-' + postid).val( JSON.stringify(sel) );
    833 	},
    834 
    835 
    836 	/**
    837 	 * Closes the image editor.
    838 	 *
    839 	 * @since 2.9.0
    840 	 *
    841 	 * @memberof imageEdit
    842 	 *
    843 	 * @param {number}  postid The post ID.
    844 	 * @param {boolean} warn   Warning message.
    845 	 *
    846 	 * @return {void|boolean} Returns false if there is a warning.
    847 	 */
    848 	close : function(postid, warn) {
    849 		warn = warn || false;
    850 
    851 		if ( warn && this.notsaved(postid) ) {
    852 			return false;
    853 		}
    854 
    855 		this.iasapi = {};
    856 		this.hold = {};
    857 
    858 		// If we've loaded the editor in the context of a Media Modal,
    859 		// then switch to the previous view, whatever that might have been.
    860 		if ( this._view ){
    861 			this._view.back();
    862 		}
    863 
    864 		// In case we are not accessing the image editor in the context of a View,
    865 		// close the editor the old-school way.
    866 		else {
    867 			$('#image-editor-' + postid).fadeOut('fast', function() {
    868 				$( '#media-head-' + postid ).fadeIn( 'fast', function() {
    869 					// Move focus back to the Edit Image button. Runs also when saving.
    870 					$( '#imgedit-open-btn-' + postid ).trigger( 'focus' );
    871 				});
    872 				$(this).empty();
    873 			});
    874 		}
    875 
    876 
    877 	},
    878 
    879 	/**
    880 	 * Checks if the image edit history is saved.
    881 	 *
    882 	 * @since 2.9.0
    883 	 *
    884 	 * @memberof imageEdit
    885 	 *
    886 	 * @param {number} postid The post ID.
    887 	 *
    888 	 * @return {boolean} Returns true if the history is not saved.
    889 	 */
    890 	notsaved : function(postid) {
    891 		var h = $('#imgedit-history-' + postid).val(),
    892 			history = ( h !== '' ) ? JSON.parse(h) : [],
    893 			pop = this.intval( $('#imgedit-undone-' + postid).val() );
    894 
    895 		if ( pop < history.length ) {
    896 			if ( confirm( $('#imgedit-leaving-' + postid).html() ) ) {
    897 				return false;
    898 			}
    899 			return true;
    900 		}
    901 		return false;
    902 	},
    903 
    904 	/**
    905 	 * Adds an image edit action to the history.
    906 	 *
    907 	 * @since 2.9.0
    908 	 *
    909 	 * @memberof imageEdit
    910 	 *
    911 	 * @param {Object} op     The original position.
    912 	 * @param {number} postid The post ID.
    913 	 * @param {string} nonce  The nonce.
    914 	 *
    915 	 * @return {void}
    916 	 */
    917 	addStep : function(op, postid, nonce) {
    918 		var t = this, elem = $('#imgedit-history-' + postid),
    919 			history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : [],
    920 			undone = $( '#imgedit-undone-' + postid ),
    921 			pop = t.intval( undone.val() );
    922 
    923 		while ( pop > 0 ) {
    924 			history.pop();
    925 			pop--;
    926 		}
    927 		undone.val(0); // Reset.
    928 
    929 		history.push(op);
    930 		elem.val( JSON.stringify(history) );
    931 
    932 		t.refreshEditor(postid, nonce, function() {
    933 			t.setDisabled($('#image-undo-' + postid), true);
    934 			t.setDisabled($('#image-redo-' + postid), false);
    935 		});
    936 	},
    937 
    938 	/**
    939 	 * Rotates the image.
    940 	 *
    941 	 * @since 2.9.0
    942 	 *
    943 	 * @memberof imageEdit
    944 	 *
    945 	 * @param {string} angle  The angle the image is rotated with.
    946 	 * @param {number} postid The post ID.
    947 	 * @param {string} nonce  The nonce.
    948 	 * @param {Object} t      The target element.
    949 	 *
    950 	 * @return {boolean}
    951 	 */
    952 	rotate : function(angle, postid, nonce, t) {
    953 		if ( $(t).hasClass('disabled') ) {
    954 			return false;
    955 		}
    956 
    957 		this.addStep({ 'r': { 'r': angle, 'fw': this.hold.h, 'fh': this.hold.w }}, postid, nonce);
    958 	},
    959 
    960 	/**
    961 	 * Flips the image.
    962 	 *
    963 	 * @since 2.9.0
    964 	 *
    965 	 * @memberof imageEdit
    966 	 *
    967 	 * @param {number} axis   The axle the image is flipped on.
    968 	 * @param {number} postid The post ID.
    969 	 * @param {string} nonce  The nonce.
    970 	 * @param {Object} t      The target element.
    971 	 *
    972 	 * @return {boolean}
    973 	 */
    974 	flip : function (axis, postid, nonce, t) {
    975 		if ( $(t).hasClass('disabled') ) {
    976 			return false;
    977 		}
    978 
    979 		this.addStep({ 'f': { 'f': axis, 'fw': this.hold.w, 'fh': this.hold.h }}, postid, nonce);
    980 	},
    981 
    982 	/**
    983 	 * Crops the image.
    984 	 *
    985 	 * @since 2.9.0
    986 	 *
    987 	 * @memberof imageEdit
    988 	 *
    989 	 * @param {number} postid The post ID.
    990 	 * @param {string} nonce  The nonce.
    991 	 * @param {Object} t      The target object.
    992 	 *
    993 	 * @return {void|boolean} Returns false if the crop button is disabled.
    994 	 */
    995 	crop : function (postid, nonce, t) {
    996 		var sel = $('#imgedit-selection-' + postid).val(),
    997 			w = this.intval( $('#imgedit-sel-width-' + postid).val() ),
    998 			h = this.intval( $('#imgedit-sel-height-' + postid).val() );
    999 
   1000 		if ( $(t).hasClass('disabled') || sel === '' ) {
   1001 			return false;
   1002 		}
   1003 
   1004 		sel = JSON.parse(sel);
   1005 		if ( sel.w > 0 && sel.h > 0 && w > 0 && h > 0 ) {
   1006 			sel.fw = w;
   1007 			sel.fh = h;
   1008 			this.addStep({ 'c': sel }, postid, nonce);
   1009 		}
   1010 
   1011 		// Clear the selection fields after cropping.
   1012 		$('#imgedit-sel-width-' + postid).val('');
   1013 		$('#imgedit-sel-height-' + postid).val('');
   1014 	},
   1015 
   1016 	/**
   1017 	 * Undoes an image edit action.
   1018 	 *
   1019 	 * @since 2.9.0
   1020 	 *
   1021 	 * @memberof imageEdit
   1022 	 *
   1023 	 * @param {number} postid   The post ID.
   1024 	 * @param {string} nonce    The nonce.
   1025 	 *
   1026 	 * @return {void|false} Returns false if the undo button is disabled.
   1027 	 */
   1028 	undo : function (postid, nonce) {
   1029 		var t = this, button = $('#image-undo-' + postid), elem = $('#imgedit-undone-' + postid),
   1030 			pop = t.intval( elem.val() ) + 1;
   1031 
   1032 		if ( button.hasClass('disabled') ) {
   1033 			return;
   1034 		}
   1035 
   1036 		elem.val(pop);
   1037 		t.refreshEditor(postid, nonce, function() {
   1038 			var elem = $('#imgedit-history-' + postid),
   1039 				history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : [];
   1040 
   1041 			t.setDisabled($('#image-redo-' + postid), true);
   1042 			t.setDisabled(button, pop < history.length);
   1043 			// When undo gets disabled, move focus to the redo button to avoid a focus loss.
   1044 			if ( history.length === pop ) {
   1045 				$( '#image-redo-' + postid ).trigger( 'focus' );
   1046 			}
   1047 		});
   1048 	},
   1049 
   1050 	/**
   1051 	 * Reverts a undo action.
   1052 	 *
   1053 	 * @since 2.9.0
   1054 	 *
   1055 	 * @memberof imageEdit
   1056 	 *
   1057 	 * @param {number} postid The post ID.
   1058 	 * @param {string} nonce  The nonce.
   1059 	 *
   1060 	 * @return {void}
   1061 	 */
   1062 	redo : function(postid, nonce) {
   1063 		var t = this, button = $('#image-redo-' + postid), elem = $('#imgedit-undone-' + postid),
   1064 			pop = t.intval( elem.val() ) - 1;
   1065 
   1066 		if ( button.hasClass('disabled') ) {
   1067 			return;
   1068 		}
   1069 
   1070 		elem.val(pop);
   1071 		t.refreshEditor(postid, nonce, function() {
   1072 			t.setDisabled($('#image-undo-' + postid), true);
   1073 			t.setDisabled(button, pop > 0);
   1074 			// When redo gets disabled, move focus to the undo button to avoid a focus loss.
   1075 			if ( 0 === pop ) {
   1076 				$( '#image-undo-' + postid ).trigger( 'focus' );
   1077 			}
   1078 		});
   1079 	},
   1080 
   1081 	/**
   1082 	 * Sets the selection for the height and width in pixels.
   1083 	 *
   1084 	 * @since 2.9.0
   1085 	 *
   1086 	 * @memberof imageEdit
   1087 	 *
   1088 	 * @param {number} postid The post ID.
   1089 	 * @param {jQuery} el     The element containing the values.
   1090 	 *
   1091 	 * @return {void|boolean} Returns false when the x or y value is lower than 1,
   1092 	 *                        void when the value is not numeric or when the operation
   1093 	 *                        is successful.
   1094 	 */
   1095 	setNumSelection : function( postid, el ) {
   1096 		var sel, elX = $('#imgedit-sel-width-' + postid), elY = $('#imgedit-sel-height-' + postid),
   1097 			x = this.intval( elX.val() ), y = this.intval( elY.val() ),
   1098 			img = $('#image-preview-' + postid), imgh = img.height(), imgw = img.width(),
   1099 			sizer = this.hold.sizer, x1, y1, x2, y2, ias = this.iasapi;
   1100 
   1101 		if ( false === this.validateNumeric( el ) ) {
   1102 			return;
   1103 		}
   1104 
   1105 		if ( x < 1 ) {
   1106 			elX.val('');
   1107 			return false;
   1108 		}
   1109 
   1110 		if ( y < 1 ) {
   1111 			elY.val('');
   1112 			return false;
   1113 		}
   1114 
   1115 		if ( x && y && ( sel = ias.getSelection() ) ) {
   1116 			x2 = sel.x1 + Math.round( x * sizer );
   1117 			y2 = sel.y1 + Math.round( y * sizer );
   1118 			x1 = sel.x1;
   1119 			y1 = sel.y1;
   1120 
   1121 			if ( x2 > imgw ) {
   1122 				x1 = 0;
   1123 				x2 = imgw;
   1124 				elX.val( Math.round( x2 / sizer ) );
   1125 			}
   1126 
   1127 			if ( y2 > imgh ) {
   1128 				y1 = 0;
   1129 				y2 = imgh;
   1130 				elY.val( Math.round( y2 / sizer ) );
   1131 			}
   1132 
   1133 			ias.setSelection( x1, y1, x2, y2 );
   1134 			ias.update();
   1135 			this.setCropSelection(postid, ias.getSelection());
   1136 		}
   1137 	},
   1138 
   1139 	/**
   1140 	 * Rounds a number to a whole.
   1141 	 *
   1142 	 * @since 2.9.0
   1143 	 *
   1144 	 * @memberof imageEdit
   1145 	 *
   1146 	 * @param {number} num The number.
   1147 	 *
   1148 	 * @return {number} The number rounded to a whole number.
   1149 	 */
   1150 	round : function(num) {
   1151 		var s;
   1152 		num = Math.round(num);
   1153 
   1154 		if ( this.hold.sizer > 0.6 ) {
   1155 			return num;
   1156 		}
   1157 
   1158 		s = num.toString().slice(-1);
   1159 
   1160 		if ( '1' === s ) {
   1161 			return num - 1;
   1162 		} else if ( '9' === s ) {
   1163 			return num + 1;
   1164 		}
   1165 
   1166 		return num;
   1167 	},
   1168 
   1169 	/**
   1170 	 * Sets a locked aspect ratio for the selection.
   1171 	 *
   1172 	 * @since 2.9.0
   1173 	 *
   1174 	 * @memberof imageEdit
   1175 	 *
   1176 	 * @param {number} postid     The post ID.
   1177 	 * @param {number} n          The ratio to set.
   1178 	 * @param {jQuery} el         The element containing the values.
   1179 	 *
   1180 	 * @return {void}
   1181 	 */
   1182 	setRatioSelection : function(postid, n, el) {
   1183 		var sel, r, x = this.intval( $('#imgedit-crop-width-' + postid).val() ),
   1184 			y = this.intval( $('#imgedit-crop-height-' + postid).val() ),
   1185 			h = $('#image-preview-' + postid).height();
   1186 
   1187 		if ( false === this.validateNumeric( el ) ) {
   1188 			this.iasapi.setOptions({
   1189 				aspectRatio: null
   1190 			});
   1191 
   1192 			return;
   1193 		}
   1194 
   1195 		if ( x && y ) {
   1196 			this.iasapi.setOptions({
   1197 				aspectRatio: x + ':' + y
   1198 			});
   1199 
   1200 			if ( sel = this.iasapi.getSelection(true) ) {
   1201 				r = Math.ceil( sel.y1 + ( ( sel.x2 - sel.x1 ) / ( x / y ) ) );
   1202 
   1203 				if ( r > h ) {
   1204 					r = h;
   1205 					if ( n ) {
   1206 						$('#imgedit-crop-height-' + postid).val('');
   1207 					} else {
   1208 						$('#imgedit-crop-width-' + postid).val('');
   1209 					}
   1210 				}
   1211 
   1212 				this.iasapi.setSelection( sel.x1, sel.y1, sel.x2, r );
   1213 				this.iasapi.update();
   1214 			}
   1215 		}
   1216 	},
   1217 
   1218 	/**
   1219 	 * Validates if a value in a jQuery.HTMLElement is numeric.
   1220 	 *
   1221 	 * @since 4.6.0
   1222 	 *
   1223 	 * @memberof imageEdit
   1224 	 *
   1225 	 * @param {jQuery} el The html element.
   1226 	 *
   1227 	 * @return {void|boolean} Returns false if the value is not numeric,
   1228 	 *                        void when it is.
   1229 	 */
   1230 	validateNumeric: function( el ) {
   1231 		if ( ! this.intval( $( el ).val() ) ) {
   1232 			$( el ).val( '' );
   1233 			return false;
   1234 		}
   1235 	}
   1236 };
   1237 })(jQuery);