diff --git a/image_widget_crop.libraries.yml b/image_widget_crop.libraries.yml index 6baf32b..1ecd61e 100644 --- a/image_widget_crop.libraries.yml +++ b/image_widget_crop.libraries.yml @@ -6,14 +6,16 @@ cropper: - core/jquery cropper.integration: - version: 1.7 + version: 2.0 js: - js/imageWidgetCrop.js: {} + js/ImageWidgetCrop.js: {} + js/ImageWidgetCropType.js: {} + js/iwc.behaviors.js: {} css: theme: css/image_widget_crop.css: {} dependencies: - core/jquery - core/drupal - - core/drupalSettings + - core/drupal.debounce - image_widget_crop/cropper diff --git a/js/ImageWidgetCropType.js b/js/ImageWidgetCropType.js new file mode 100644 index 0000000..9a48dd2 --- /dev/null +++ b/js/ImageWidgetCropType.js @@ -0,0 +1,737 @@ +/** + * @file + * Defines the behaviors needed for cropper integration. + */ + +(function ($, Drupal) { + 'use strict'; + + /** + * @class Drupal.ImageWidgetCropType + * + * @param {Drupal.ImageWidgetCrop} instance + * The main ImageWidgetCrop instance that created this one. + * + * @param {HTMLElement|jQuery} element + * The wrapper element. + */ + Drupal.ImageWidgetCropType = function (instance, element) { + + /** + * The ImageWidgetCrop instance responsible for creating this type. + * + * @type {Drupal.ImageWidgetCrop} + */ + this.instance = instance; + + /** + * The Cropper plugin wrapper element. + * + * @type {jQuery} + */ + this.$cropperWrapper = $(); + + /** + * The wrapper element. + * + * @type {jQuery} + */ + this.$wrapper = $(element); + + /** + * The table element, if any. + * + * @type {jQuery} + */ + this.$table = this.$wrapper.find(this.selectors.table); + + /** + * The image element. + * + * @type {jQuery} + */ + this.$image = this.$wrapper.find(this.selectors.image); + + /** + * The reset element. + * + * @type {jQuery} + */ + this.$reset = this.$wrapper.find(this.selectors.reset); + + /** + * @type {Cropper} + */ + this.cropper = null; + + /** + * Flag indicating whether this instance is enabled. + * + * @type {Boolean} + */ + this.enabled = true; + + /** + * The hard limit of the crop. + * + * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}} + */ + this.hardLimit = { + height: null, + width: null, + reached: { + height: false, + width: false + } + }; + + /** + * The unique identifier for this ImageWidgetCrop type. + * + * @type {String} + */ + this.id = null; + + /** + * Flag indicating whether the instance has been initialized. + * + * @type {Boolean} + */ + this.initialized = false; + + /** + * An object of recorded setInterval instances. + * + * @type {Object.} + */ + this.intervals = {}; + + /** + * The delta ratio of image based on its natural dimensions. + * + * @type {Number} + */ + this.naturalDelta = null; + + /** + * The natural height of the image. + * + * @type {Number} + */ + this.naturalHeight = null; + + /** + * The natural width of the image. + * + * @type {Number} + */ + this.naturalWidth = null; + + /** + * The original height of the image. + * + * @type {Number} + */ + this.originalHeight = 0; + + /** + * The original width of the image. + * + * @type {Number} + */ + this.originalWidth = 0; + + /** + * The current Cropper options. + * + * @type {Cropper.options} + */ + this.options = {}; + + /** + * Flag indicating whether to show the default crop. + * + * @type {Boolean} + */ + this.showDefaultCrop = true; + + /** + * The soft limit of the crop. + * + * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}} + */ + this.softLimit = { + height: null, + width: null, + reached: { + height: false, + width: false + } + }; + + /** + * The numeric representation of a ratio. + * + * @type {Number} + */ + this.ratio = NaN; + + /** + * The value elements. + * + * @type {Object.} + */ + this.values = { + applied: this.$wrapper.find(this.selectors.values.applied), + height: this.$wrapper.find(this.selectors.values.height), + width: this.$wrapper.find(this.selectors.values.width), + x: this.$wrapper.find(this.selectors.values.x), + y: this.$wrapper.find(this.selectors.values.y) + }; + + /** + * Flag indicating whether the instance is currently visible. + * + * @type {Boolean} + */ + this.visible = false; + + // Initialize the instance. + this.init(); + }; + + /** + * The prefix used for all Image Widget Crop data attributes. + * + * @type {RegExp} + */ + Drupal.ImageWidgetCropType.prototype.dataPrefix = /^drupalIwc/; + + /** + * Default options to pass to the Cropper plugin. + * + * @type {Object} + */ + Drupal.ImageWidgetCropType.prototype.defaultOptions = { + autoCropArea: 1, + background: false, + responsive: false, + viewMode: 1, + zoomable: false + }; + + /** + * The selectors used to identify elements for this module. + * + * @type {Object} + */ + Drupal.ImageWidgetCropType.prototype.selectors = { + image: '[data-drupal-iwc=image]', + reset: '[data-drupal-iwc=reset]', + table: '[data-drupal-iwc=table]', // @todo is this even used anymore? + values: { + applied: '[data-drupal-iwc-value=applied]', + height: '[data-drupal-iwc-value=height]', + width: '[data-drupal-iwc-value=width]', + x: '[data-drupal-iwc-value=x]', + y: '[data-drupal-iwc-value=y]' + } + }; + + /** + * The "built" event handler for the Cropper plugin. + */ + Drupal.ImageWidgetCropType.prototype.built = function () { + this.$cropperWrapper = this.$wrapper.find('.cropper-container'); + this.updateHardLimits(); + this.updateSoftLimits(); + }; + + /** + * The "cropend" event handler for the Cropper plugin. + */ + Drupal.ImageWidgetCropType.prototype.cropEnd = function () { + // Immediately return if there is no cropper instance (for whatever reason). + if (!this.cropper) { + return; + } + + // Retrieve the cropper data. + var data = this.cropper.getData(); + + // Ensure the applied state is enabled. + data.applied = 1; + + // Data returned by Cropper plugin should be multiplied with delta in order + // to get the proper crop sizes for the original image. + this.setValues(data, this.naturalDelta); + + // Trigger summary updates. + this.$wrapper.trigger('summaryUpdated'); + }; + + /** + * The "cropmove" event handler for the Cropper plugin. + */ + Drupal.ImageWidgetCropType.prototype.cropMove = function () { + this.updateSoftLimits(); + }; + + /** + * Destroys this instance. + */ + Drupal.ImageWidgetCropType.prototype.destroy = function () { + this.destroyCropper(); + + this.$image.off('.iwc'); + this.$reset.off('.iwc'); + + // Clear any intervals that were set. + for (var interval in this.intervals) { + if (this.intervals.hasOwnProperty(interval)) { + clearInterval(interval); + delete this.intervals[interval]; + } + } + }; + + /** + * Destroys the Cropper plugin instance. + */ + Drupal.ImageWidgetCropType.prototype.destroyCropper = function () { + this.$image.off('.iwc.cropper'); + if (this.cropper) { + this.cropper.destroy(); + this.cropper = null; + } + }; + + /** + * Disables this instance. + */ + Drupal.ImageWidgetCropType.prototype.disable = function () { + if (this.cropper) { + this.cropper.disable(); + } + this.$table.removeClass('responsive-enabled--opened'); + }; + + /** + * Enables this instance. + */ + Drupal.ImageWidgetCropType.prototype.enable = function () { + if (this.cropper) { + this.cropper.enable(); + } + this.$table.addClass('responsive-enabled--opened'); + }; + + /** + * Retrieves a crop value. + * + * @param {'applied'|'height'|'width'|'x'|'y'} name + * The name of the crop value to retrieve. + * @param {Number} [delta] + * The delta amount to divide value by, if any. + * + * @return {Number} + * The crop value. + */ + Drupal.ImageWidgetCropType.prototype.getValue = function (name, delta) { + var value = 0; + if (this.values[name] && this.values[name][0]) { + value = parseInt(this.values[name][0].value, 10) || 0; + } + return name !== 'applied' && value && delta ? Math.round(value / delta) : value; + }; + + /** + * Retrieves all crop values. + * + * @param {Number} [delta] + * The delta amount to divide value by, if any. + * + * @return {{applied: Number, height: Number, width: Number, x: Number, y: Number}} + * The crop value key/value pairs. + */ + Drupal.ImageWidgetCropType.prototype.getValues = function (delta) { + var values = {}; + for (var name in this.values) { + if (this.values.hasOwnProperty(name)) { + values[name] = this.getValue(name, delta); + } + } + return values; + }; + + /** + * Initializes the instance. + */ + Drupal.ImageWidgetCropType.prototype.init = function () { + // Immediately return if already initialized. + if (this.initialized) { + return; + } + + // Set the default options. + this.options = $.extend({}, this.defaultOptions); + + // Extend this instance with data from the wrapper. + var data = this.$wrapper.data(); + for (var i in data) { + if (data.hasOwnProperty(i) && this.dataPrefix.test(i)) { + // Remove Drupal + module prefix and lowercase the first letter. + var prop = i.replace(this.dataPrefix, ''); + prop = prop.charAt(0).toLowerCase() + prop.slice(1); + + // Check if data attribute exists on this object. + if (prop && this.hasOwnProperty(prop)) { + var value = data[i]; + + // Parse the ratio value. + if (prop === 'ratio') { + value = this.parseRatio(value); + } + this[prop] = typeof value === 'object' ? $.extend(true, {}, this[prop], value) : value; + } + } + } + + // Bind necessary events. + this.$image + .on('visible.iwc', function () { + this.visible = true; + this.naturalHeight = parseInt(this.$image.prop('naturalHeight'), 10); + this.naturalWidth = parseInt(this.$image.prop('naturalWidth'), 10); + // Calculate delta between original and thumbnail images. + this.naturalDelta = this.originalHeight && this.naturalHeight ? this.originalHeight / this.naturalHeight : null; + }.bind(this)) + // Only initialize the cropper plugin once. + .one('visible.iwc', this.initializeCropper.bind(this)) + .on('hidden.iwc', function () { + this.visible = false; + }.bind(this)) + ; + + this.$reset + .on('click.iwc', this.reset.bind(this)) + ; + + // Star polling visibility of the image that should be able to be cropped. + this.pollVisibility(this.$image); + + // Bind the drupalSetSummary callback. + this.$wrapper.drupalSetSummary(this.updateSummary.bind(this)); + + // Trigger the initial summaryUpdate event. + this.$wrapper.trigger('summaryUpdated'); + }; + + /** + * Initializes the Cropper plugin. + */ + Drupal.ImageWidgetCropType.prototype.initializeCropper = function () { + // Calculate minimal height for cropper container (minimal width is 200). + var minDelta = (this.originalWidth / 200); + this.options.minContainerHeight = this.originalHeight / minDelta; + + // Only autoCrop if 'Show default crop' is checked. + this.options.autoCrop = this.showDefaultCrop; + + // Set aspect ratio. + this.options.aspectRatio = this.ratio; + + // Initialize data. + var values = this.getValues(this.naturalDelta); + this.options.data = this.options.data || {}; + if (values.applied) { + // Remove the "applied" value as it has no meaning in Cropper. + delete values.applied; + + // Merge in the values. + this.options.data = $.extend(true, this.options.data, values); + + // Enforce autoCrop if there's currently a crop applied. + this.options.autoCrop = true; + } + + this.options.data.rotate = 0; + this.options.data.scaleX = 1; + this.options.data.scaleY = 1; + + this.$image + .on('built.iwc.cropper', this.built.bind(this)) + .on('cropend.iwc.cropper', this.cropEnd.bind(this)) + .on('cropmove.iwc.cropper', this.cropMove.bind(this)) + .cropper(this.options) + ; + + this.cropper = this.$image.data('cropper'); + this.options = this.cropper.options; + + // If "Show default crop" is checked apply default crop. + if (this.showDefaultCrop) { + // All data returned by cropper plugin multiple with delta in order to get + // proper crop sizes for original image. + this.setValue(this.$image.cropper('getData'), this.naturalDelta); + this.$wrapper.trigger('summaryUpdated'); + } + }; + + /** + * Creates a poll that checks visibility of an item. + * + * @param {HTMLElement|jQuery} element + * The element to poll. + * + * @todo Perhaps replace once vertical tabs have proper events? + * + * @see https://www.drupal.org/node/2653570 + */ + Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) { + var $element = $(element); + var value = null; + var interval = setInterval(function () { + var visible = $element.is(':visible'); + if (value !== visible) { + $element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc'); + } + }, 250); + this.intervals[interval] = $element; + }; + + /** + * Parses a ration value into a numeric one. + * + * @param {String} ratio + * A string representation of the ratio. + * + * @return {Number.|NaN} + * The numeric representation of the ratio. + */ + Drupal.ImageWidgetCropType.prototype.parseRatio = function (ratio) { + if (ratio && /:/.test(ratio)) { + var parts = ratio.split(':'); + var num1 = parseInt(parts[0], 10); + var num2 = parseInt(parts[1], 10); + return num1 / num2; + } + return parseFloat(ratio); + }; + + /** + * Reset cropping for an element. + * + * @param {Event} e + * The event object. + */ + Drupal.ImageWidgetCropType.prototype.reset = function (e) { + if (!this.cropper) { + return; + } + + if (e instanceof Event || e instanceof $.Event) { + e.preventDefault(); + e.stopPropagation(); + } + + this.options = $.extend({}, this.cropper.options, this.defaultOptions); + + var delta = null; + + // Retrieve all current values and zero (0) them out. + var values = this.getValues(); + for (var name in values) { + if (values.hasOwnProperty(name)) { + values[name] = 0; + } + } + + // If 'Show default crop' is not checked just re-initialize the cropper. + if (!this.showDefaultCrop) { + this.destroyCropper(); + this.initializeCropper(); + } + // Reset cropper to the original values. + else { + this.cropper.reset(); + this.cropper.options = this.options; + + // Set the delta. + delta = this.naturalDelta; + + // Merge in the original cropper values. + values = $.extend(values, this.cropper.getData()); + } + + this.setValues(values, delta); + this.$wrapper.trigger('summaryUpdated'); + }; + + /** + * The "resize" event handler proxied from the main instance. + * + * @see Drupal.ImageWidgetCrop.prototype.resize + */ + Drupal.ImageWidgetCropType.prototype.resize = function () { + // Immediately return if currently not visible. + if (!this.visible) { + return; + } + + // Get previous data for cropper. + var canvasDataOld = this.$image.cropper('getCanvasData'); + var cropBoxData = this.$image.cropper('getCropBoxData'); + + // Re-render cropper. + this.$image.cropper('render'); + + // Get new data for cropper and calculate resize ratio. + var canvasDataNew = this.$image.cropper('getCanvasData'); + var ratio = 1; + if (canvasDataOld.width !== 0) { + ratio = canvasDataNew.width / canvasDataOld.width; + } + + // Set new data for crop box. + $.each(cropBoxData, function (index, value) { + cropBoxData[index] = value * ratio; + }); + this.$image.cropper('setCropBoxData', cropBoxData); + + this.updateHardLimits(); + this.updateSoftLimits(); + this.$wrapper.trigger('summaryUpdated'); + }; + + /** + * Sets a single crop value. + * + * @param {'applied'|'height'|'width'|'x'|'y'} name + * The name of the crop value to set. + * @param {Number} value + * The value to set. + * @param {Number} [delta] + * A delta to modify the value with. + */ + Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) { + if (!this.values.hasOwnProperty(name) || !this.values[name][0]) { + return; + } + value = value ? parseInt(value, 10) : 0; + if (delta && name !== 'applied') { + value = Math.round(value * delta); + } + this.values[name][0].value = value; + this.values[name].trigger('change.iwc, input.iwc'); + }; + + /** + * Sets multiple crop values. + * + * @param {{applied: Number, height: Number, width: Number, x: Number, y: Number}} obj + * An object of key/value pairs of values to set. + * @param {Number} [delta] + * A delta to modify the value with. + */ + Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) { + for (var name in obj) { + if (!obj.hasOwnProperty(name)) { + continue; + } + this.setValue(name, obj[name], delta); + } + }; + + /** + * Converts horizontal and vertical dimensions to canvas dimensions. + * + * @param {Number} x - horizontal dimension in image space. + * @param {Number} y - vertical dimension in image space. + */ + Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) { + var imageData = this.cropper.getImageData(); + return { + width: imageData.width * (x / this.originalWidth), + height: imageData.height * (y / this.originalHeight) + } + }; + + /** + * Converts horizontal and vertical dimensions to image dimensions. + * + * @param {Number} x - horizontal dimension in canvas space. + * @param {Number} y - vertical dimension in canvas space. + */ + Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) { + var imageData = this.cropper.getImageData(); + return { + width: x * (this.originalWidth / imageData.width), + height: y * (this.originalHeight / imageData.height) + } + }; + + /** + * Update hard limits. + */ + Drupal.ImageWidgetCropType.prototype.updateHardLimits = function () { + // Immediately return if there is no cropper plugin instance or hard limits. + if (!this.cropper || !this.hardLimit.width || !this.hardLimit.height) { + return; + } + + var options = this.cropper.options; + + // Limits works in canvas so we need to convert dimensions. + var converted = this.toCanvasDimensions(this.hardLimit.width, this.hardLimit.height); + options.minCropBoxWidth = converted.width; + options.minCropBoxHeight = converted.height; + + // After updating the options we need to limit crop box. + this.cropper.limitCropBox(true, false); + }; + + /** + * Update soft limits. + */ + Drupal.ImageWidgetCropType.prototype.updateSoftLimits = function () { + // Immediately return if there is no cropper plugin instance or soft limits. + if (!this.cropper || !this.softLimit.width || !this.softLimit.height) { + return; + } + + // We do comparison in image dimensions so lets convert first. + var cropBoxData = this.cropper.getCropBoxData(); + var converted = this.toImageDimensions(cropBoxData.width, cropBoxData.height); + + var dimensions = ['width', 'height']; + for (var i = 0, l = dimensions.length; i < l; i++) { + var dimension = dimensions[i]; + if (converted[dimension] < this.softLimit[dimension]) { + if (!this.softLimit.reached[dimension]) { + this.softLimit.reached[dimension] = true; + } + } + else if (this.softLimit.reached[dimension]) { + this.softLimit.reached[dimension] = false; + } + this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]); + } + this.$wrapper.trigger('summaryUpdated'); + }; + + /** + * Updates the summary of the wrapper. + */ + Drupal.ImageWidgetCropType.prototype.updateSummary = function () { + var summary = []; + if (this.getValue('applied')) { + summary.push(Drupal.t('Cropping applied.')); + } + if (this.softLimit.reached.height || this.softLimit.reached.width) { + summary.push(Drupal.t('Soft limit reached.')); + } + return summary.join('
'); + }; + +}(jQuery, Drupal)); diff --git a/js/imageWidgetCrop.js b/js/imageWidgetCrop.js index e1ff363..b2f36a1 100644 --- a/js/imageWidgetCrop.js +++ b/js/imageWidgetCrop.js @@ -3,497 +3,138 @@ * Defines the behaviors needed for cropper integration. */ -(function ($, Drupal, drupalSettings) { +(function ($, Drupal, debounce) { 'use strict'; - var cropperSelector = '.crop-preview-wrapper__preview-image'; - var cropperValuesSelector = '.crop-preview-wrapper__value'; - var cropWrapperSelector = '.image-data__crop-wrapper'; - var cropWrapperSummarySelector = 'div > a[role="button"], summary'; - var verticalTabsSelector = '.vertical-tabs'; - var verticalTabsMenuItemSelector = '.vertical-tabs__menu-item, .vertical-tab-button'; - var resetSelector = '.crop-preview-wrapper__crop-reset'; - var detailsWrapper = cropWrapperSelector + ' > div:first-child'; - var detailsParentSelector = '.image-widget-data'; - var table = '.responsive-enabled'; - var boostrapTable = '.panel-body.panel-collapse'; - var cropperOptions = { - background: false, - zoomable: false, - viewMode: 1, - autoCropArea: 1, - responsive: false, - // Callback function, fires when crop is applied. - cropend: function (e) { - var $this = $(this); - var $values = $this.siblings(cropperValuesSelector); - var data = $this.cropper('getData'); - // Calculate delta between original and thumbnail images. - var delta = $this.data('original-height') / $this.prop('naturalHeight'); - /* - * All data returned by cropper plugin multiple with delta in order to get - * proper crop sizes for original image. - */ - $values.find('.crop-x').val(Math.round(data.x * delta)); - $values.find('.crop-y').val(Math.round(data.y * delta)); - $values.find('.crop-width').val(Math.round(data.width * delta)); - $values.find('.crop-height').val(Math.round(data.height * delta)); - $values.find('.crop-applied').val(1); - Drupal.imageWidgetCrop.updateCropSummaries($this); - } - }; - - Drupal.imageWidgetCrop = {}; + var $window = $(window); /** - * Initialize cropper on the ImageWidgetCrop widget. + * @class Drupal.ImageWidgetCrop * - * @param {Object} context - Element to initialize cropper on. + * @param {HTMLElement|jQuery} element + * The wrapper element. */ - Drupal.imageWidgetCrop.initialize = function (context) { - var $cropWrapper = $(cropWrapperSelector, context); - var $cropWrapperSummary = $cropWrapper.children(detailsWrapper).find(cropWrapperSummarySelector); - var $verticalTabs = $(verticalTabsSelector, context); - var $verticalTabsMenuItem = $verticalTabs.find(verticalTabsMenuItemSelector); - var $reset = $(resetSelector, context); + Drupal.ImageWidgetCrop = function (element) { - /* - * Cropper initialization on click events on vertical tabs and details - * summaries (for smaller screens). + /** + * The wrapper element. + * + * @type {jQuery} */ - $verticalTabsMenuItem.add($cropWrapperSummary).click(function () { - var tabId = $(this).find('a').attr('href'); - var $cropper = $(this).parent().find(cropperSelector); - if (typeof tabId !== 'undefined') { - $cropper = $(tabId).find(cropperSelector); - } - var ratio = Drupal.imageWidgetCrop.getRatio($cropper); - Drupal.imageWidgetCrop.initializeCropper($cropper, ratio); - }); - - // Handling click event for opening/closing vertical tabs, we use "find" instead "children" to support other themes. - $cropWrapper.find(cropWrapperSummarySelector).once('imageWidgetCrop').click(function (evt) { - // Work only on bigger screens where $verticalTabsMenuItem is not empty. - if ($verticalTabsMenuItem.length !== 0) { - // If detailsWrapper is not visible display it and initialize cropper. - if (!$(this).siblings(detailsWrapper).is(':visible')) { - evt.preventDefault(); - // We check if the "structure" of element are more "standard" or have changed. - if ($(this).parent().is('details')) { - $(this).parent().attr('open','open'); - $(table).addClass('responsive-enabled--opened'); - $(this).parent().find(detailsWrapper).show(); - Drupal.imageWidgetCrop.initializeCropperOnChildren($(this).parent()); - } else { - // To support boostrap theme we need to add specifics, attributes required by them @see #2803407 - $(this).attr('aria-expanded', 'true'); - $(boostrapTable).addClass('in'); - $(boostrapTable).css('height', ''); - // Boostrap theme add two level in element, ATM that work but found better way... - $(this).parent().parent().find(detailsWrapper).show(); - Drupal.imageWidgetCrop.initializeCropperOnChildren($(this).parent().parent()); - } - evt.stopImmediatePropagation(); - } - // If detailsWrapper is visible hide it. - else { - $(this).parent().removeAttr('open'); - $(table).removeClass('responsive-enabled--opened'); - $(this).parent().find(detailsWrapper).hide(); - } - } - }); - - $reset.on('click', function (e) { - e.preventDefault(); - var $element = $(this).siblings(cropperSelector); - Drupal.imageWidgetCrop.reset($element); - return false; - }); - - // Handling cropping when viewport resizes. - $(window).resize(function () { - $(detailsParentSelector).each(function () { - // Find only opened widgets. - var cropperDetailsWrapper = $(this).children('details[open="open"], .image-data__crop-wrapper > div[aria-expanded="true"]'); - cropperDetailsWrapper.each(function () { - // Find all croppers for opened widgets. - var $croppers = $(this).find(cropperSelector); - $croppers.each(function () { - var $this = $(this); - if ($this.parent().parent().parent().css('display') !== 'none') { - // Get previous data for cropper. - var canvasDataOld = $this.cropper('getCanvasData'); - var cropBoxData = $this.cropper('getCropBoxData'); + this.$wrapper = $(element); - // Re-render cropper. - $this.cropper('render'); + /** + * The summary element. + * + * @type {jQuery} + */ + this.$summary = this.$wrapper.find(this.selectors.summary).first(); - // Get new data for cropper and calculate resize ratio. - var canvasDataNew = $this.cropper('getCanvasData'); - var ratio = 1; - if (canvasDataOld.width !== 0) { - ratio = canvasDataNew.width / canvasDataOld.width; - } + /** + * Flag indicating whether the instance has been initialized. + * + * @type {Boolean} + */ + this.initialized = false; - // Set new data for crop box. - $.each(cropBoxData, function (index, value) { - cropBoxData[index] = value * ratio; - }); - $this.cropper('setCropBoxData', cropBoxData); + /** + * The current summary text. + * + * @type {String} + */ + this.summary = Drupal.t('Crop image'); - Drupal.imageWidgetCrop.updateHardLimits($this); - Drupal.imageWidgetCrop.checkSoftLimits($this); - Drupal.imageWidgetCrop.updateCropSummaries($this); - } - }); - }); - }); - }); + /** + * The individual ImageWidgetCropType instances. + * + * @type {Array.} + */ + this.types = []; - // Correctly updating messages of summaries. - Drupal.imageWidgetCrop.updateAllCropSummaries(); + // Initialize the instance. + this.init(); }; /** - * Get ratio data and determine if an available ratio or free crop. + * The selectors used to identify elements for this module. * - * @param {Object} $element - Element to initialize cropper on its children. + * @type {Object.} */ - Drupal.imageWidgetCrop.getRatio = function ($element) { - var ratio = $element.data('ratio'); - var regex = /:/; - - if ((regex.exec(ratio)) !== null) { - var int = ratio.split(":"); - if ($.isArray(int) && ($.isNumeric(int[0]) && $.isNumeric(int[1]))) { - return int[0] / int[1]; - } - else { - return "NaN"; - } - } - else { - return ratio; - } + Drupal.ImageWidgetCrop.prototype.selectors = { + // Unfortunately, core does not provide a way to inject attributes into the + // wrapper element's "summary" in a stable way. So, we can only target + // the immediate children known to be the likely elements. Contrib can + // extend this selector if needed. + summary: '> summary, > legend', + types: '[data-drupal-iwc=type]', + wrapper: '[data-drupal-iwc=wrapper]' }; /** - * Initialize cropper on an element. - * - * @param {Object} $element - Element to initialize cropper on. - * @param {number} ratio - The ratio of the image. + * Destroys this instance. */ - Drupal.imageWidgetCrop.initializeCropper = function ($element, ratio) { - var data = null; - var $values = $element.siblings(cropperValuesSelector); - - // Calculate minimal height for cropper container (minimal width is 200). - var minDelta = ($element.data('original-width') / 200); - cropperOptions['minContainerHeight'] = $element.data('original-height') / minDelta; - - var options = cropperOptions; - var delta = $element.data('original-height') / $element.prop('naturalHeight'); - - // If 'Show default crop' is checked show crop box. - options.autoCrop = drupalSettings['crop_default']; - - if (parseInt($values.find('.crop-applied').val()) === 1) { - data = { - x: Math.round(parseInt($values.find('.crop-x').val()) / delta), - y: Math.round(parseInt($values.find('.crop-y').val()) / delta), - width: Math.round(parseInt($values.find('.crop-width').val()) / delta), - height: Math.round(parseInt($values.find('.crop-height').val()) / delta), - rotate: 0, - scaleX: 1, - scaleY: 1 - }; - options.autoCrop = true; - } - - // React on crop move and check soft limits. - options.cropmove = function (e) { - Drupal.imageWidgetCrop.checkSoftLimits($(this)); - }; - - options.data = data; - options.aspectRatio = ratio; - - $element.cropper(options); - - // Hard and soft limits we need to check for fist time when cropper - // finished it initialization. - $element.on('built.cropper', function (e) { - var $this = $(this); - Drupal.imageWidgetCrop.updateHardLimits($this); - Drupal.imageWidgetCrop.checkSoftLimits($this); + Drupal.ImageWidgetCrop.prototype.destroy = function () { + this.types.forEach(function (type) { + type.destroy(); }); - - // If 'Show default crop' is checked apply default crop. - if (drupalSettings['crop_default']) { - var dataDefault = $element.cropper('getData'); - // Calculate delta between original and thumbnail images. - var deltaDefault = $element.data('original-height') / $element.prop('naturalHeight'); - /* - * All data returned by cropper plugin multiple with delta in order to get - * proper crop sizes for original image. - */ - Drupal.imageWidgetCrop.updateCropValues($values, dataDefault, deltaDefault); - Drupal.imageWidgetCrop.updateCropSummaries($element); - } - }; - - /** - * Update crop values in hidden inputs. - * - * @param {Object} $element - Cropper values selector. - * @param {Array} $data - Cropper data. - * @param {number} $delta - Delta between original and thumbnail images. - */ - Drupal.imageWidgetCrop.updateCropValues = function ($element, $data, $delta) { - $element.find('.crop-x').val(Math.round($data.x * $delta)); - $element.find('.crop-y').val(Math.round($data.y * $delta)); - $element.find('.crop-width').val(Math.round($data.width * $delta)); - $element.find('.crop-height').val(Math.round($data.height * $delta)); - $element.find('.crop-applied').val(1); - }; - - /** - * Converts horizontal and vertical dimensions to canvas dimensions. - * - * @param {Object} $element - Crop element. - * @param {Number} x - horizontal dimension in image space. - * @param {Number} y - vertical dimension in image space. - */ - Drupal.imageWidgetCrop.toCanvasDimensions = function ($element, x, y) { - var imageData = $element.data('cropper').getImageData(); - return { - width: imageData.width * (x / $element.data('original-width')), - height: imageData.height * (y / $element.data('original-height')) - } - }; - - /** - * Converts horizontal and vertical dimensions to image dimensions. - * - * @param {Object} $element - Crop element. - * @param {Number} x - horizontal dimension in canvas space. - * @param {Number} y - vertical dimension in canvas space. - */ - Drupal.imageWidgetCrop.toImageDimensions = function ($element, x, y) { - var imageData = $element.data('cropper').getImageData(); - return { - width: x * ($element.data('original-width') / imageData.width), - height: y * ($element.data('original-height') / imageData.height) - } - }; - - /** - * Update hard limits for given element. - * - * @param {Object} $element - Crop element. - */ - Drupal.imageWidgetCrop.updateHardLimits = function ($element) { - var cropName = $element.data('name'); - - // Check first that we have configuration for this crop. - if (!drupalSettings.image_widget_crop.hasOwnProperty(cropName)) { - return; - } - - var cropConfig = drupalSettings.image_widget_crop[cropName]; - var cropper = $element.data('cropper'); - var options = cropper.options; - - // Limits works in canvas so we need to convert dimensions. - var converted = Drupal.imageWidgetCrop.toCanvasDimensions($element, cropConfig.hard_limit.width, cropConfig.hard_limit.height); - options.minCropBoxWidth = converted.width; - options.minCropBoxHeight = converted.height; - - // After updating the options we need to limit crop box. - cropper.limitCropBox(true, false); }; /** - * Check soft limit for given crop element. - * - * @param {Object} $element - Crop element. + * Initializes the instance. */ - Drupal.imageWidgetCrop.checkSoftLimits = function ($element) { - var cropName = $element.data('name'); - - // Check first that we have configuration for this crop. - if (!drupalSettings.image_widget_crop.hasOwnProperty(cropName)) { + Drupal.ImageWidgetCrop.prototype.init = function () { + if (this.initialized) { return; } - var cropConfig = drupalSettings.image_widget_crop[cropName]; - - var minSoftCropBox = { - 'width': Number(cropConfig.soft_limit.width) || 0, - 'height': Number(cropConfig.soft_limit.height) || 0 - }; - - // We do comparison in image dimensions so lets convert first. - var cropBoxData = $element.cropper('getCropBoxData'); - var converted = Drupal.imageWidgetCrop.toImageDimensions($element, cropBoxData.width, cropBoxData.height); - - var dimensions = ['width', 'height']; - - for (var i = 0; i < dimensions.length; ++i) { - // @todo - setting up soft limit status in data attribute is not ideal - // but current architecture is like that. When we convert to proper - // one imageWidgetCrop object per crop widget we will be able to fix - // this also. @see https://www.drupal.org/node/2660788. - var softLimitReached = $element.data(dimensions[i] + '-soft-limit-reached'); - - if (converted[dimensions[i]] < minSoftCropBox[dimensions[i]]) { - if (!softLimitReached) { - softLimitReached = true; - Drupal.imageWidgetCrop.softLimitChanged($element, dimensions[i], softLimitReached); - } - } - else if (softLimitReached) { - softLimitReached = false; - Drupal.imageWidgetCrop.softLimitChanged($element, dimensions[i], softLimitReached); - } - } - }; - - /** - * React on soft limit change. - * - * @param {Object} $element - Crop element. - * @param {boolean} newSoftLimitState - new soft imit state, true if it - * reached, or false. - */ - Drupal.imageWidgetCrop.softLimitChanged = function ($element, dimension, newSoftLimitState) { - var $cropperWrapper = $element.siblings('.cropper-container'); - if (newSoftLimitState) { - $cropperWrapper.addClass('cropper--' + dimension + '-soft-limit-reached'); - } - else { - $cropperWrapper.removeClass('cropper--' + dimension + '-soft-limit-reached'); - } - - // @todo - use temporary storage while we are waiting for [#2660788]. - $element.data(dimension + '-soft-limit-reached', newSoftLimitState); - - Drupal.imageWidgetCrop.updateSingleCropSummary($element); - }; - - /** - * Initialize cropper on all children of an element. - * - * @param {Object} $element - Element to initialize cropper on its children. - */ - Drupal.imageWidgetCrop.initializeCropperOnChildren = function ($element) { - var visibleCropper = $element.find(cropperSelector + ':visible'); - var ratio = Drupal.imageWidgetCrop.getRatio($(visibleCropper)); - Drupal.imageWidgetCrop.initializeCropper($(visibleCropper), ratio); - }; - - /** - * Update single crop summary of an element. - * - * @param {Object} $element - The element cropping on which has been changed. - */ - Drupal.imageWidgetCrop.updateSingleCropSummary = function ($element) { - var $values = $element.siblings(cropperValuesSelector); - var croppingApplied = parseInt($values.find('.crop-applied').val()); - var summaryMessages = []; + // Find all the types. + var _this = this; + this.$wrapper.find(this.selectors.types).each(function () { + _this.types.push(new Drupal.ImageWidgetCropType(_this, this)); + }); - $element.closest('details').drupalSetSummary(function (context) { - if (croppingApplied === 1) { - summaryMessages.push(Drupal.t('Cropping applied.')); - } + // Debounce resize event to prevent any issues. + $window.on('resize.iwc', debounce(this.resize.bind(this), 250)); - if ($element.data('height-soft-limit-reached') || $element.data('width-soft-limit-reached')) { - summaryMessages.push(Drupal.t('Soft limit reached.')); - } + // Update the summary when triggered from vertical tabs underneath it. + this.$wrapper.on('summaryUpdated', this.updateSummary.bind(this)); - return summaryMessages.join('
'); - }); + // Trigger the initial summaryUpdate event. + this.$wrapper.trigger('summaryUpdated'); }; /** - * Update common crop summary of an element. + * The "resize" event callback. * - * @param {Object} $element - The element cropping on which has been changed. + * @see Drupal.ImageWidgetCropType.prototype.resize */ - Drupal.imageWidgetCrop.updateCommonCropSummary = function ($element) { - var croppingApplied = parseInt($element.find('.crop-applied[value="1"]').length); - var wrapperText = Drupal.t('Crop image'); - if (croppingApplied) { - wrapperText = Drupal.t('Crop image (cropping applied)'); - } - $element.find(cropWrapperSummarySelector).text(wrapperText); - }; + Drupal.ImageWidgetCrop.prototype.resize = function () { + var args = arguments; - /** - * Update crop summaries after cropping cas been set or reset. - * - * @param {Object} $element - The element cropping on which has been changed. - */ - Drupal.imageWidgetCrop.updateCropSummaries = function ($element) { - var $details = $element.closest('details' + cropWrapperSelector); - Drupal.imageWidgetCrop.updateSingleCropSummary($element); - Drupal.imageWidgetCrop.updateCommonCropSummary($details); - }; - - /** - * Update crop summaries of all elements. - */ - Drupal.imageWidgetCrop.updateAllCropSummaries = function () { - var $croppers = $(cropperSelector); - $croppers.each(function () { - Drupal.imageWidgetCrop.updateSingleCropSummary($(this)); - }); - var $cropWrappers = $(cropWrapperSelector); - $cropWrappers.each(function () { - Drupal.imageWidgetCrop.updateCommonCropSummary($(this)); + // Proxy the resize event to each ImageWidgetCropType instance. + this.types.forEach(function (type) { + type.resize.apply(type, args); }); }; /** - * Reset cropping for an element. - * - * @param {Object} $element - The element to reset cropping on. + * Updates the summary of the wrapper. */ - Drupal.imageWidgetCrop.reset = function ($element) { - var $valuesDefault = $element.siblings(cropperValuesSelector); - var options = cropperOptions; - // If 'Show default crop' is not checked re-initialize cropper. - if (!drupalSettings['crop_default']) { - $element.cropper('destroy'); - options.autoCrop = false; - $element.cropper(options); - $valuesDefault.find('.crop-applied').val(0); - $valuesDefault.find('.crop-x').val(''); - $valuesDefault.find('.crop-y').val(''); - $valuesDefault.find('.crop-width').val(''); - $valuesDefault.find('.crop-height').val(''); - } - else { - // Reset cropper. - $element.cropper('reset').cropper('options', options); - var dataDefault = $element.cropper('getData'); - // Calculate delta between original and thumbnail images. - var deltaDefault = $element.data('original-height') / $element.prop('naturalHeight'); - /* - * All data returned by cropper plugin multiple with delta in order to get - * proper crop sizes for original image. - */ - Drupal.imageWidgetCrop.updateCropValues($valuesDefault, dataDefault, deltaDefault); + Drupal.ImageWidgetCrop.prototype.updateSummary = function () { + var text = Drupal.t('Crop image'); + + // Determine if any ImageWidgetCropType has been applied. + for (var i = 0, l = this.types.length; i < l; i++) { + var type = this.types[i]; + if (type.getValue('applied')) { + text = Drupal.t('Crop image (cropping applied)'); + break; + } } - Drupal.imageWidgetCrop.updateCropSummaries($element); - }; - Drupal.behaviors.imageWidgetCrop = { - attach: function (context) { - Drupal.imageWidgetCrop.initialize(context); - Drupal.imageWidgetCrop.updateAllCropSummaries(); + if (this.summary !== text) { + this.$summary.text(this.summary = text); } }; -}(jQuery, Drupal, drupalSettings)); +}(jQuery, Drupal, Drupal.debounce)); diff --git a/js/iwc.behaviors.js b/js/iwc.behaviors.js new file mode 100644 index 0000000..2714adc --- /dev/null +++ b/js/iwc.behaviors.js @@ -0,0 +1,62 @@ +/** + * @file + * Defines the Drupal behaviors needed for the Image Widget Crop module. + */ + +(function ($, Drupal) { + 'use strict'; + + /** + * Drupal behavior for the Image Widget Crop module. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the behavior and creates Cropper instances. + * @prop {Drupal~behaviorAttach} detach + * Detaches the behavior and destroys Cropper instances. + */ + Drupal.behaviors.imageWidgetCrop = { + attach: function (context) { + this.createInstances(context); + }, + detach: function (context) { + this.destroyInstances(context); + }, + + /** + * Creates necessary instances of Drupal.ImageWidgetCrop. + * + * @param {HTMLElement|jQuery} [context=document] + * The context which to find elements in. + */ + createInstances: function (context) { + var $context = $(context || document); + $context.find(Drupal.ImageWidgetCrop.prototype.selectors.wrapper).each(function () { + var $element = $(this); + if (!$element.data('ImageWidgetCrop')) { + $element.data('ImageWidgetCrop', new Drupal.ImageWidgetCrop($element)); + } + }); + }, + + /** + * Destroys any instances of Drupal.ImageWidgetCrop. + * + * @param {HTMLElement|jQuery} [context=document] + * The context which to find elements in. + */ + destroyInstances: function (context) { + var $context = $(context || document); + $context.find(Drupal.ImageWidgetCrop.prototype.selectors.wrapper).each(function () { + var $element = $(this); + var instance = $element.data('ImageWidgetCrop'); + if (instance) { + instance.destroy(); + $element.removeData('ImageWidgetCrop'); + } + }); + } + }; + +}(jQuery, Drupal)); diff --git a/src/Element/ImageCrop.php b/src/Element/ImageCrop.php index a83ec0d..3f9d50b 100644 --- a/src/Element/ImageCrop.php +++ b/src/Element/ImageCrop.php @@ -2,6 +2,7 @@ namespace Drupal\image_widget_crop\Element; +use Drupal\Component\Serialization\Json; use Drupal\Core\Render\Element\FormElement; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -58,8 +59,6 @@ class ImageCrop extends FormElement { /** @var \Drupal\file\Entity\File $file */ $file = $element['#file']; if (!empty($file) && preg_match('/image/', $file->getMimeType())) { - $element['#attached']['drupalSettings']['crop_default'] = $element['#show_default_crop']; - /** @var \Drupal\Core\Image\Image $image */ $image = \Drupal::service('image.factory')->get($file->getFileUri()); if (!$image->isValid()) { @@ -88,7 +87,10 @@ class ImageCrop extends FormElement { $element['crop_wrapper'] = [ '#type' => 'details', '#title' => t('Crop image'), - '#attributes' => ['class' => ['image-data__crop-wrapper']], + '#attributes' => [ + 'class' => ['image-data__crop-wrapper'], + 'data-drupal-iwc' => 'wrapper', + ], '#open' => $element['#show_crop_area'], '#weight' => 100, ]; @@ -113,72 +115,73 @@ class ImageCrop extends FormElement { $element['crop_wrapper'][$list_id] = [ '#type' => 'vertical_tabs', - '#theme_wrappers' => ['vertical_tabs'], '#parents' => [$list_id], ]; /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $crop_type_storage */ $crop_type_storage = \Drupal::entityTypeManager()->getStorage('crop_type'); - if (!empty($crop_type_storage->loadMultiple())) { - foreach ($crop_type_list as $crop_type) { - /** @var \Drupal\crop\Entity\CropType $crop_type */ - $crop_type = $crop_type_storage->load($crop_type); - $ratio = $crop_type->getAspectRatio() ? $crop_type->getAspectRatio() : 'Nan'; - - $element['#attached']['drupalSettings']['image_widget_crop'][$crop_type->id()] = [ - 'soft_limit' => $crop_type->getSoftLimit(), - 'hard_limit' => $crop_type->getHardLimit(), - ]; - $element['crop_wrapper'][$crop_type->id()] = [ + /** @var \Drupal\crop\Entity\CropType[] $crop_types */ + if ($crop_types = $crop_type_storage->loadMultiple($crop_type_list)) { + foreach ($crop_types as $type => $crop_type) { + $ratio = $crop_type->getAspectRatio() ?: 'NaN'; + + $element['crop_wrapper'][$type] = [ '#type' => 'details', '#title' => $crop_type->label(), '#group' => $list_id, + '#attributes' => [ + 'data-drupal-iwc' => 'type', + 'data-drupal-iwc-id' => $type, + 'data-drupal-iwc-ratio' => $ratio, + 'data-drupal-iwc-show-default-crop' => $element['#show_default_crop'] ? 'true' : 'false', + 'data-drupal-iwc-soft-limit' => Json::encode($crop_type->getSoftLimit()), + 'data-drupal-iwc-hard-limit' => Json::encode($crop_type->getHardLimit()), + 'data-drupal-iwc-original-width' => ($file instanceof FileEntity) ? $file->getMetadata('width') : getimagesize($file->getFileUri())[0], + 'data-drupal-iwc-original-height' => ($file instanceof FileEntity) ? $file->getMetadata('height') : getimagesize($file->getFileUri())[1], + ], ]; // Generation of html List with image & crop information. - $element['crop_wrapper'][$crop_type->id()]['crop_container'] = [ + $element['crop_wrapper'][$type]['crop_container'] = [ + '#id' => $type, '#type' => 'container', - '#attributes' => [ - 'class' => ['crop-preview-wrapper', $list_id], - 'id' => [$crop_type->id()], - 'data-ratio' => [$ratio], - ], + '#attributes' => ['class' => ['crop-preview-wrapper', $list_id]], '#weight' => -10, ]; - $element['crop_wrapper'][$crop_type->id()]['crop_container']['image'] = [ + $element['crop_wrapper'][$type]['crop_container']['image'] = [ '#theme' => 'image_style', '#style_name' => $element['#crop_preview_image_style'], '#attributes' => [ 'class' => ['crop-preview-wrapper__preview-image'], - 'data-ratio' => $ratio, - 'data-name' => $crop_type->id(), - 'data-original-width' => ($file instanceof FileEntity) ? $file->getMetadata('width') : getimagesize($file->getFileUri())[0], - 'data-original-height' => ($file instanceof FileEntity) ? $file->getMetadata('height') : getimagesize($file->getFileUri())[1], + 'data-drupal-iwc' => 'image', ], '#uri' => $file->getFileUri(), '#weight' => -10, ]; - $element['crop_wrapper'][$crop_type->id()]['crop_container']['reset'] = [ + $element['crop_wrapper'][$type]['crop_container']['reset'] = [ '#type' => 'button', '#value' => t('Reset crop'), - '#attributes' => ['class' => ['crop-preview-wrapper__crop-reset']], + '#attributes' => [ + 'class' => ['crop-preview-wrapper__crop-reset'], + 'data-drupal-iwc' => 'reset', + ], '#weight' => -10, ]; // Generation of html List with image & crop information. - $element['crop_wrapper'][$crop_type->id()]['crop_container']['values'] = [ + $element['crop_wrapper'][$type]['crop_container']['values'] = [ '#type' => 'container', '#attributes' => ['class' => ['crop-preview-wrapper__value']], '#weight' => -9, ]; // Element to track whether cropping is applied or not. - $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied'] = [ + $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied'] = [ '#type' => 'hidden', - '#attributes' => ['class' => ["crop-applied"]], + '#attributes' => ['data-drupal-iwc-value' => 'applied'], '#default_value' => 0, ]; $edit = FALSE; @@ -186,17 +189,17 @@ class ImageCrop extends FormElement { $form_state_element_values = $form_state->getValue($element['#parents']); // Check if form state has values. if ($form_state_element_values) { - $form_state_properties = $form_state_element_values['crop_wrapper'][$crop_type->id()]['crop_container']['values']; + $form_state_properties = $form_state_element_values['crop_wrapper'][$type]['crop_container']['values']; // If crop is applied by the form state we keep it that way. if ($form_state_properties['crop_applied'] == '1') { - $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 1; + $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied']['#default_value'] = 1; $edit = TRUE; } $properties = $form_state_properties; } /** @var \Drupal\crop\Entity\Crop $crop */ - $crop = Crop::findCrop($file->getFileUri(), $crop_type->id()); + $crop = Crop::findCrop($file->getFileUri(), $type); if ($crop) { $edit = TRUE; /** @var \Drupal\image_widget_crop\ImageWidgetCropManager $image_widget_crop_manager */ @@ -207,14 +210,14 @@ class ImageCrop extends FormElement { // form state has no values yet and there are saved values then we // use the saved values. $properties = $original_properties == $properties || empty($properties) ? $original_properties : $properties; - $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 1; + $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied']['#default_value'] = 1; // If the user edits an entity and while adding new images resets an // saved crop we keep it reset. if (isset($properties['crop_applied']) && $properties['crop_applied'] == '0') { - $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 0; + $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied']['#default_value'] = 0; } } - self::getCropFormElement($element, 'crop_container', $properties, $edit, $crop_type->id()); + self::getCropFormElement($element, 'crop_container', $properties, $edit, $type); } // Stock Original File Values. $element['file-uri'] = [ @@ -275,9 +278,7 @@ class ImageCrop extends FormElement { $value_property = self::getCropFormPropertyValue($element, $crop_type_id, $edit, $value['value'], $property); $crop_element = [ '#type' => 'hidden', - '#attributes' => [ - 'class' => ["crop-$property"], - ], + '#attributes' => ['data-drupal-iwc-value' => $property], '#crop_type' => $crop_type_id, '#element_name' => $property, '#default_value' => $value_property,