diff -r 490d5cc509ed -r cf61fcea0001 wp/wp-admin/js/image-edit.js --- a/wp/wp-admin/js/image-edit.js Tue Jun 09 11:14:17 2015 +0000 +++ b/wp/wp-admin/js/image-edit.js Mon Oct 14 17:39:30 2019 +0200 @@ -1,26 +1,81 @@ /* global imageEditL10n, ajaxurl, confirm */ +/** + * @summary The functions necessary for editing images. + * + * @since 2.9.0 + */ (function($) { -var imageEdit = window.imageEdit = { + + /** + * Contains all the methods to initialise and control the image editor. + * + * @namespace imageEdit + */ + var imageEdit = window.imageEdit = { iasapi : {}, hold : {}, postid : '', _view : false, + /** + * @summary Converts a value to an integer. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} f The float value that should be converted. + * + * @return {number} The integer representation from the float value. + */ intval : function(f) { + /* + * Bitwise OR operator: one of the obscure ways to truncate floating point figures, + * worth reminding JavaScript doesn't have a distinct "integer" type. + */ return f | 0; }, - setDisabled : function(el, s) { + /** + * @summary Adds the disabled attribute and class to a single form element + * or a field set. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {jQuery} el The element that should be modified. + * @param {bool|number} s The state for the element. If set to true + * the element is disabled, + * otherwise the element is enabled. + * The function is sometimes called with a 0 or 1 + * instead of true or false. + * + * @returns {void} + */ + setDisabled : function( el, s ) { + /* + * `el` can be a single form element or a fieldset. Before #28864, the disabled state on + * some text fields was handled targeting $('input', el). Now we need to handle the + * disabled state on buttons too so we can just target `el` regardless if it's a single + * element or a fieldset because when a fieldset is disabled, its descendants are disabled too. + */ if ( s ) { - el.removeClass('disabled'); - $('input', el).removeAttr('disabled'); + el.removeClass( 'disabled' ).prop( 'disabled', false ); } else { - el.addClass('disabled'); - $('input', el).prop('disabled', true); + el.addClass( 'disabled' ).prop( 'disabled', true ); } }, + /** + * @summary Initializes the image editor. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * + * @returns {void} + */ init : function(postid) { var t = this, old = $('#image-editor-' + t.postid), x = t.intval( $('#imgedit-x-' + postid).val() ), @@ -40,10 +95,12 @@ $('input[type="text"]', '#imgedit-panel-' + postid).keypress(function(e) { var k = e.keyCode; + // Key codes 37 thru 40 are the arrow keys. if ( 36 < k && k < 41 ) { $(this).blur(); } + // The key code 13 is the enter key. if ( 13 === k ) { e.preventDefault(); e.stopPropagation(); @@ -52,29 +109,87 @@ }); }, + /** + * @summary Toggles the wait/load icon in the editor. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {number} toggle Is 0 or 1, fades the icon in then 1 and out when 0. + * + * @returns {void} + */ toggleEditor : function(postid, toggle) { var wait = $('#imgedit-wait-' + postid); if ( toggle ) { - wait.height( $('#imgedit-panel-' + postid).height() ).fadeIn('fast'); + wait.fadeIn( 'fast' ); } else { wait.fadeOut('fast'); } }, + /** + * @summary Shows or hides the image edit help box. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {HTMLElement} el The element to create the help window in. + * + * @returns {boolean} Always returns false. + */ toggleHelp : function(el) { - $( el ).parents( '.imgedit-group-top' ).toggleClass( 'imgedit-help-toggled' ).find( '.imgedit-help' ).slideToggle( 'fast' ); + var $el = $( el ); + $el + .attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' ) + .parents( '.imgedit-group-top' ).toggleClass( 'imgedit-help-toggled' ).find( '.imgedit-help' ).slideToggle( 'fast' ); + return false; }, + /** + * @summary Gets the value from the image edit target. + * + * The image edit target contains the image sizes where the (possible) changes + * have to be applied to. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * + * @returns {string} The value from the imagedit-save-target input field when available, + * or 'full' when not available. + */ getTarget : function(postid) { return $('input[name="imgedit-target-' + postid + '"]:checked', '#imgedit-save-target-' + postid).val() || 'full'; }, - scaleChanged : function(postid, x) { + /** + * @summary Recalculates the height or width and keeps the original aspect ratio. + * + * If the original image size is exceeded a red exclamation mark is shown. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The current post id. + * @param {number} x Is 0 when it applies the y-axis + * and 1 when applicable for the x-axis. + * @param {jQuery} el Element. + * + * @returns {void} + */ + scaleChanged : function( postid, x, el ) { var w = $('#imgedit-scale-width-' + postid), h = $('#imgedit-scale-height-' + postid), warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = ''; + if ( false === this.validateNumeric( el ) ) { + return; + } + if ( x ) { h1 = ( w.val() !== '' ) ? Math.round( w.val() / this.hold.xy_ratio ) : ''; h.val( h1 ); @@ -90,6 +205,16 @@ } }, + /** + * @summary Gets the selected aspect ratio. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * + * @returns {string} The aspect ratio. + */ getSelRatio : function(postid) { var x = this.hold.w, y = this.hold.h, X = this.intval( $('#imgedit-crop-width-' + postid).val() ), @@ -106,11 +231,24 @@ return '1:1'; }, + /** + * @summary Removes the last action from the image edit history + * The history consist of (edit) actions performed on the image. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {number} setSize 0 or 1, when 1 the image resets to its original size. + * + * @returns {string} JSON string containing the history or an empty string if no history exists. + */ filterHistory : function(postid, setSize) { - // apply undo state to history + // Apply undo state to history. var history = $('#imgedit-history-' + postid).val(), pop, n, o, i, op = []; if ( history !== '' ) { + // Read the JSON string with the image edit history. history = JSON.parse(history); pop = this.intval( $('#imgedit-undone-' + postid).val() ); if ( pop > 0 ) { @@ -120,6 +258,7 @@ } } + // Reset size to it's original state. if ( setSize ) { if ( !history.length ) { this.hold.w = this.hold.ow; @@ -127,17 +266,21 @@ return ''; } - // restore + // Restore original 'o'. o = history[history.length - 1]; + + // c = 'crop', r = 'rotate', f = 'flip' o = o.c || o.r || o.f || false; if ( o ) { + // fw = Full image width this.hold.w = o.fw; + // fh = Full image height this.hold.h = o.fh; } } - // filter the values + // Filter the last step/action from the history. for ( n in history ) { i = history[n]; if ( i.hasOwnProperty('c') ) { @@ -152,7 +295,20 @@ } return ''; }, - + /** + * @summary Binds the necessary events to the image. + * + * When the image source is reloaded the image will be reloaded. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {string} nonce The nonce to verify the request. + * @param {function} callback Function to execute when the image is loaded. + * + * @returns {void} + */ refreshEditor : function(postid, nonce, callback) { var t = this, data, img; @@ -165,9 +321,27 @@ 'rand': t.intval(Math.random() * 1000000) }; - img = $('') - .on('load', function() { - var max1, max2, parent = $('#imgedit-crop-' + postid), t = imageEdit; + img = $( '' ) + .on( 'load', { history: data.history }, function( event ) { + var max1, max2, + parent = $( '#imgedit-crop-' + postid ), + t = imageEdit, + historyObj; + + // Checks if there already is some image-edit history. + if ( '' !== event.data.history ) { + historyObj = JSON.parse( event.data.history ); + // If last executed action in history is a crop action. + if ( historyObj[historyObj.length - 1].hasOwnProperty( 'c' ) ) { + /* + * A crop action has completed and the crop button gets disabled + * ensure the undo button is enabled. + */ + t.setDisabled( $( '#image-undo-' + postid) , true ); + // Move focus to the undo button to avoid a focus loss. + $( '#image-undo-' + postid ).focus(); + } + } parent.empty().append(img); @@ -197,7 +371,22 @@ }) .attr('src', ajaxurl + '?' + $.param(data)); }, - + /** + * @summary Performs an image edit action. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {string} nonce The nonce to verify the request. + * @param {string} action The action to perform on the image. + * The possible actions are: "scale" and "restore". + * + * @returns {boolean|void} Executes a post request that refreshes the page + * when the action is performed. + * Returns false if a invalid action is given, + * or when the action cannot be performed. + */ action : function(postid, nonce, action) { var t = this, data, w, h, fw, fh; @@ -249,6 +438,19 @@ }); }, + /** + * @summary Stores the changes that are made to the image. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id to get the image from the database. + * @param {string} nonce The nonce to verify the request. + * + * @returns {boolean|void} If the actions are successfully saved a response message is shown. + * Returns false if there is no image editing history, + * thus there are not edit-actions performed on the image. + */ save : function(postid, nonce) { var data, target = this.getTarget(postid), @@ -269,10 +471,12 @@ 'context': $('#image-edit-context').length ? $('#image-edit-context').val() : null, 'do': 'save' }; - + // Post the image edit data to the backend. $.post(ajaxurl, data, function(r) { + // Read the response. var ret = JSON.parse(r); + // If a response is returned, close the editor and show an error. if ( ret.error ) { $('#imgedit-response-' + postid).html('

' + ret.error + '

'); imageEdit.close(postid); @@ -299,13 +503,33 @@ }); }, + /** + * @summary Creates the image edit window. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id for the image. + * @param {string} nonce The nonce to verify the request. + * @param {object} view The image editor view to be used for the editing. + * + * @returns {void|promise} Either returns void if the button was already activated + * or returns an instance of the image editor, wrapped in a promise. + */ open : function( postid, nonce, view ) { this._view = view; var dfd, data, elem = $('#image-editor-' + postid), head = $('#media-head-' + postid), btn = $('#imgedit-open-btn-' + postid), spin = btn.siblings('.spinner'); - btn.prop('disabled', true); + /* + * Instead of disabling the button, which causes a focus loss and makes screen + * readers announce "unavailable", return if the button was already clicked. + */ + if ( btn.hasClass( 'button-activated' ) ) { + return; + } + spin.addClass( 'is-active' ); data = { @@ -318,27 +542,61 @@ dfd = $.ajax({ url: ajaxurl, type: 'post', - data: data + data: data, + beforeSend: function() { + btn.addClass( 'button-activated' ); + } }).done(function( html ) { elem.html( html ); head.fadeOut('fast', function(){ elem.fadeIn('fast'); - btn.removeAttr('disabled'); + btn.removeClass( 'button-activated' ); spin.removeClass( 'is-active' ); }); + // Initialise the Image Editor now that everything is ready. + imageEdit.init( postid ); }); return dfd; }, + /** + * @summary Initializes the cropping tool and sets a default cropping selection. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * + * @returns {void} + */ imgLoaded : function(postid) { var img = $('#image-preview-' + postid), parent = $('#imgedit-crop-' + postid); + // Ensure init has run even when directly loaded. + if ( 'undefined' === typeof this.hold.sizer ) { + this.init( postid ); + } + this.initCrop(postid, img, parent); this.setCropSelection(postid, 0); this.toggleEditor(postid, 0); + // Editor is ready, move focus to the first focusable element. + $( '.imgedit-wrap .imgedit-help-toggle' ).eq( 0 ).focus(); }, + /** + * @summary Initializes the cropping tool. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {HTMLElement} image The preview image. + * @param {HTMLElement} parent The preview image container. + * + * @returns {void} + */ initCrop : function(postid, image, parent) { var t = this, selW = $('#imgedit-sel-width-' + postid), @@ -353,14 +611,23 @@ minWidth: 3, minHeight: 3, + /** + * @summary Sets the CSS styles and binds events for locking the aspect ratio. + * + * @param {jQuery} img The preview image. + */ onInit: function( img ) { - // Ensure that the imgareaselect wrapper elements are position:absolute + // Ensure that the imgAreaSelect wrapper elements are position:absolute. // (even if we're in a position:fixed modal) $img = $( img ); $img.next().css( 'position', 'absolute' ) .nextAll( '.imgareaselect-outer' ).css( 'position', 'absolute' ); - - parent.children().mousedown(function(e){ + /** + * @summary Binds mouse down event to the cropping container. + * + * @returns {void} + */ + parent.children().on( 'mousedown, touchstart', function(e){ var ratio = false, sel, defRatio; if ( e.shiftKey ) { @@ -375,14 +642,34 @@ }); }, + /** + * @summary Event triggered when starting a selection. + * + * @returns {void} + */ onSelectStart: function() { imageEdit.setDisabled($('#imgedit-crop-sel-' + postid), 1); }, - + /** + * @summary Event triggered when the selection is ended. + * + * @param {object} img jQuery object representing the image. + * @param {object} c The selection. + * + * @returns {object} + */ onSelectEnd: function(img, c) { imageEdit.setCropSelection(postid, c); }, + /** + * @summary Event triggered when the selection changes. + * + * @param {object} img jQuery object representing the image. + * @param {object} c The selection. + * + * @returns {void} + */ onSelectChange: function(img, c) { var sizer = imageEdit.hold.sizer; selW.val( imageEdit.round(c.width / sizer) ); @@ -391,6 +678,17 @@ }); }, + /** + * @summary Stores the current crop selection. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {object} c The selection. + * + * @returns {boolean} + */ setCropSelection : function(postid, c) { var sel; @@ -410,6 +708,18 @@ $('#imgedit-selection-' + postid).val( JSON.stringify(sel) ); }, + + /** + * @summary Closes the image editor. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {bool} warn Warning message. + * + * @returns {void|bool} Returns false if there is a warning. + */ close : function(postid, warn) { warn = warn || false; @@ -429,7 +739,10 @@ // In case we are not accessing the image editor in the context of a View, close the editor the old-skool way else { $('#image-editor-' + postid).fadeOut('fast', function() { - $('#media-head-' + postid).fadeIn('fast'); + $( '#media-head-' + postid ).fadeIn( 'fast', function() { + // Move focus back to the Edit Image button. Runs also when saving. + $( '#imgedit-open-btn-' + postid ).focus(); + }); $(this).empty(); }); } @@ -437,6 +750,16 @@ }, + /** + * @summary Checks if the image edit history is saved. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * + * @returns {boolean} Returns true if the history is not saved. + */ notsaved : function(postid) { var h = $('#imgedit-history-' + postid).val(), history = ( h !== '' ) ? JSON.parse(h) : [], @@ -451,11 +774,23 @@ return false; }, + /** + * @summary Adds an image edit action to the history. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {object} op The original position. + * @param {number} postid The post id. + * @param {string} nonce The nonce. + * + * @returns {void} + */ addStep : function(op, postid, nonce) { var t = this, elem = $('#imgedit-history-' + postid), - history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : [], - undone = $('#imgedit-undone-' + postid), - pop = t.intval(undone.val()); + history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : [], + undone = $( '#imgedit-undone-' + postid ), + pop = t.intval( undone.val() ); while ( pop > 0 ) { history.pop(); @@ -472,6 +807,19 @@ }); }, + /** + * @summary Rotates the image. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {string} angle The angle the image is rotated with. + * @param {number} postid The post id. + * @param {string} nonce The nonce. + * @param {object} t The target element. + * + * @returns {boolean} + */ rotate : function(angle, postid, nonce, t) { if ( $(t).hasClass('disabled') ) { return false; @@ -480,6 +828,19 @@ this.addStep({ 'r': { 'r': angle, 'fw': this.hold.h, 'fh': this.hold.w }}, postid, nonce); }, + /** + * @summary Flips the image. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} axis The axle the image is flipped on. + * @param {number} postid The post id. + * @param {string} nonce The nonce. + * @param {object} t The target element. + * + * @returns {boolean} + */ flip : function (axis, postid, nonce, t) { if ( $(t).hasClass('disabled') ) { return false; @@ -488,6 +849,18 @@ this.addStep({ 'f': { 'f': axis, 'fw': this.hold.w, 'fh': this.hold.h }}, postid, nonce); }, + /** + * @summary Crops the image. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {string} nonce The nonce. + * @param {object} t The target object. + * + * @returns {void|boolean} Returns false if the crop button is disabled. + */ crop : function (postid, nonce, t) { var sel = $('#imgedit-selection-' + postid).val(), w = this.intval( $('#imgedit-sel-width-' + postid).val() ), @@ -505,6 +878,17 @@ } }, + /** + * @summary Undoes an image edit action. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {string} nonce The nonce. + * + * @returns {void|false} Returns false if the undo button is disabled. + */ undo : function (postid, nonce) { var t = this, button = $('#image-undo-' + postid), elem = $('#imgedit-undone-' + postid), pop = t.intval( elem.val() ) + 1; @@ -516,13 +900,28 @@ elem.val(pop); t.refreshEditor(postid, nonce, function() { var elem = $('#imgedit-history-' + postid), - history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : []; + history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : []; t.setDisabled($('#image-redo-' + postid), true); t.setDisabled(button, pop < history.length); + // When undo gets disabled, move focus to the redo button to avoid a focus loss. + if ( history.length === pop ) { + $( '#image-redo-' + postid ).focus(); + } }); }, + /** + * Reverts a undo action. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {string} nonce The nonce. + * + * @returns {void} + */ redo : function(postid, nonce) { var t = this, button = $('#image-redo-' + postid), elem = $('#imgedit-undone-' + postid), pop = t.intval( elem.val() ) - 1; @@ -535,15 +934,36 @@ t.refreshEditor(postid, nonce, function() { t.setDisabled($('#image-undo-' + postid), true); t.setDisabled(button, pop > 0); + // When redo gets disabled, move focus to the undo button to avoid a focus loss. + if ( 0 === pop ) { + $( '#image-undo-' + postid ).focus(); + } }); }, - setNumSelection : function(postid) { + /** + * @summary Sets the selection for the height and width in pixels. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {jQuery} el The element containing the values. + * + * @returns {void|boolean} Returns false when the x or y value is lower than 1, + * void when the value is not numeric or when the operation + * is successful. + */ + setNumSelection : function( postid, el ) { var sel, elX = $('#imgedit-sel-width-' + postid), elY = $('#imgedit-sel-height-' + postid), x = this.intval( elX.val() ), y = this.intval( elY.val() ), img = $('#image-preview-' + postid), imgh = img.height(), imgw = img.width(), sizer = this.hold.sizer, x1, y1, x2, y2, ias = this.iasapi; + if ( false === this.validateNumeric( el ) ) { + return; + } + if ( x < 1 ) { elX.val(''); return false; @@ -578,6 +998,16 @@ } }, + /** + * Rounds a number to a whole. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} num The number. + * + * @returns {number} The number rounded to a whole number. + */ round : function(num) { var s; num = Math.round(num); @@ -597,13 +1027,24 @@ return num; }, + /** + * Sets a locked aspect ratio for the selection. + * + * @memberof imageEdit + * @since 2.9.0 + * + * @param {number} postid The post id. + * @param {number} n The ratio to set. + * @param {jQuery} el The element containing the values. + * + * @returns {void} + */ setRatioSelection : function(postid, n, el) { var sel, r, x = this.intval( $('#imgedit-crop-width-' + postid).val() ), y = this.intval( $('#imgedit-crop-height-' + postid).val() ), h = $('#image-preview-' + postid).height(); - if ( !this.intval( $(el).val() ) ) { - $(el).val(''); + if ( false === this.validateNumeric( el ) ) { return; } @@ -628,6 +1069,24 @@ this.iasapi.update(); } } + }, + + /** + * Validates if a value in a jQuery.HTMLElement is numeric. + * + * @memberof imageEdit + * @since 4.6 + * + * @param {jQuery} el The html element. + * + * @returns {void|boolean} Returns false if the value is not numeric, + * void when it is. + */ + validateNumeric: function( el ) { + if ( ! this.intval( $( el ).val() ) ) { + $( el ).val( '' ); + return false; + } } }; })(jQuery);