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