[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> image-edit.js (source)

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


Generated : Wed Sep 9 08:20:27 2026 Cross-referenced by PHPXref