--- /dev/null
+/**
+ * @file
+ * Defines the behaviors needed for cropper integration.
+ */
+
+(function ($, Drupal, drupalSettings) {
+ '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 = {};
+
+ /**
+ * Initialize cropper on the ImageWidgetCrop widget.
+ *
+ * @param {Object} context - Element to initialize cropper on.
+ */
+ 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);
+
+ /*
+ * Cropper initialization on click events on vertical tabs and details
+ * summaries (for smaller screens).
+ */
+ $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');
+
+ // Re-render cropper.
+ $this.cropper('render');
+
+ // 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;
+ }
+
+ // Set new data for crop box.
+ $.each(cropBoxData, function (index, value) {
+ cropBoxData[index] = value * ratio;
+ });
+ $this.cropper('setCropBoxData', cropBoxData);
+
+ Drupal.imageWidgetCrop.updateHardLimits($this);
+ Drupal.imageWidgetCrop.checkSoftLimits($this);
+ Drupal.imageWidgetCrop.updateCropSummaries($this);
+ }
+ });
+ });
+ });
+ });
+
+ // Correctly updating messages of summaries.
+ Drupal.imageWidgetCrop.updateAllCropSummaries();
+ };
+
+ /**
+ * Get ratio data and determine if an available ratio or free crop.
+ *
+ * @param {Object} $element - Element to initialize cropper on its children.
+ */
+ 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;
+ }
+ };
+
+ /**
+ * Initialize cropper on an element.
+ *
+ * @param {Object} $element - Element to initialize cropper on.
+ * @param {number} ratio - The ratio of the image.
+ */
+ 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);
+ });
+
+ // 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.
+ */
+ 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)) {
+ 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 = [];
+
+ $element.closest('details').drupalSetSummary(function (context) {
+ if (croppingApplied === 1) {
+ summaryMessages.push(Drupal.t('Cropping applied.'));
+ }
+
+ if ($element.data('height-soft-limit-reached') || $element.data('width-soft-limit-reached')) {
+ summaryMessages.push(Drupal.t('Soft limit reached.'));
+ }
+
+ return summaryMessages.join('<br>');
+ });
+ };
+
+ /**
+ * Update common crop summary of an element.
+ *
+ * @param {Object} $element - The element cropping on which has been changed.
+ */
+ 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);
+ };
+
+ /**
+ * 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));
+ });
+ };
+
+ /**
+ * Reset cropping for an element.
+ *
+ * @param {Object} $element - The element to reset cropping on.
+ */
+ 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.updateCropSummaries($element);
+ };
+
+ Drupal.behaviors.imageWidgetCrop = {
+ attach: function (context) {
+ Drupal.imageWidgetCrop.initialize(context);
+ Drupal.imageWidgetCrop.updateAllCropSummaries();
+ }
+ };
+
+}(jQuery, Drupal, drupalSettings));