3 * Defines the behaviors needed for cropper integration.
6 (function ($, Drupal, drupalSettings) {
9 var cropperSelector = '.crop-preview-wrapper__preview-image';
10 var cropperValuesSelector = '.crop-preview-wrapper__value';
11 var cropWrapperSelector = '.image-data__crop-wrapper';
12 var cropWrapperSummarySelector = 'div > a[role="button"], summary';
13 var verticalTabsSelector = '.vertical-tabs';
14 var verticalTabsMenuItemSelector = '.vertical-tabs__menu-item, .vertical-tab-button';
15 var resetSelector = '.crop-preview-wrapper__crop-reset';
16 var detailsWrapper = cropWrapperSelector + ' > div:first-child';
17 var detailsParentSelector = '.image-widget-data';
18 var table = '.responsive-enabled';
19 var boostrapTable = '.panel-body.panel-collapse';
20 var cropperOptions = {
26 // Callback function, fires when crop is applied.
27 cropend: function (e) {
29 var $values = $this.siblings(cropperValuesSelector);
30 var data = $this.cropper('getData');
31 // Calculate delta between original and thumbnail images.
32 var delta = $this.data('original-height') / $this.prop('naturalHeight');
34 * All data returned by cropper plugin multiple with delta in order to get
35 * proper crop sizes for original image.
37 $values.find('.crop-x').val(Math.round(data.x * delta));
38 $values.find('.crop-y').val(Math.round(data.y * delta));
39 $values.find('.crop-width').val(Math.round(data.width * delta));
40 $values.find('.crop-height').val(Math.round(data.height * delta));
41 $values.find('.crop-applied').val(1);
42 Drupal.imageWidgetCrop.updateCropSummaries($this);
46 Drupal.imageWidgetCrop = {};
49 * Initialize cropper on the ImageWidgetCrop widget.
51 * @param {Object} context - Element to initialize cropper on.
53 Drupal.imageWidgetCrop.initialize = function (context) {
54 var $cropWrapper = $(cropWrapperSelector, context);
55 var $cropWrapperSummary = $cropWrapper.children(detailsWrapper).find(cropWrapperSummarySelector);
56 var $verticalTabs = $(verticalTabsSelector, context);
57 var $verticalTabsMenuItem = $verticalTabs.find(verticalTabsMenuItemSelector);
58 var $reset = $(resetSelector, context);
61 * Cropper initialization on click events on vertical tabs and details
62 * summaries (for smaller screens).
64 $verticalTabsMenuItem.add($cropWrapperSummary).click(function () {
65 var tabId = $(this).find('a').attr('href');
66 var $cropper = $(this).parent().find(cropperSelector);
67 if (typeof tabId !== 'undefined') {
68 $cropper = $(tabId).find(cropperSelector);
70 var ratio = Drupal.imageWidgetCrop.getRatio($cropper);
71 Drupal.imageWidgetCrop.initializeCropper($cropper, ratio);
74 // Handling click event for opening/closing vertical tabs, we use "find" instead "children" to support other themes.
75 $cropWrapper.find(cropWrapperSummarySelector).once('imageWidgetCrop').click(function (evt) {
76 // Work only on bigger screens where $verticalTabsMenuItem is not empty.
77 if ($verticalTabsMenuItem.length !== 0) {
78 // If detailsWrapper is not visible display it and initialize cropper.
79 if (!$(this).siblings(detailsWrapper).is(':visible')) {
81 // We check if the "structure" of element are more "standard" or have changed.
82 if ($(this).parent().is('details')) {
83 $(this).parent().attr('open','open');
84 $(table).addClass('responsive-enabled--opened');
85 $(this).parent().find(detailsWrapper).show();
86 Drupal.imageWidgetCrop.initializeCropperOnChildren($(this).parent());
88 // To support boostrap theme we need to add specifics, attributes required by them @see #2803407
89 $(this).attr('aria-expanded', 'true');
90 $(boostrapTable).addClass('in');
91 $(boostrapTable).css('height', '');
92 // Boostrap theme add two level in element, ATM that work but found better way...
93 $(this).parent().parent().find(detailsWrapper).show();
94 Drupal.imageWidgetCrop.initializeCropperOnChildren($(this).parent().parent());
96 evt.stopImmediatePropagation();
98 // If detailsWrapper is visible hide it.
100 $(this).parent().removeAttr('open');
101 $(table).removeClass('responsive-enabled--opened');
102 $(this).parent().find(detailsWrapper).hide();
107 $reset.on('click', function (e) {
109 var $element = $(this).siblings(cropperSelector);
110 Drupal.imageWidgetCrop.reset($element);
114 // Handling cropping when viewport resizes.
115 $(window).resize(function () {
116 $(detailsParentSelector).each(function () {
117 // Find only opened widgets.
118 var cropperDetailsWrapper = $(this).children('details[open="open"], .image-data__crop-wrapper > div[aria-expanded="true"]');
119 cropperDetailsWrapper.each(function () {
120 // Find all croppers for opened widgets.
121 var $croppers = $(this).find(cropperSelector);
122 $croppers.each(function () {
124 if ($this.parent().parent().parent().css('display') !== 'none') {
125 // Get previous data for cropper.
126 var canvasDataOld = $this.cropper('getCanvasData');
127 var cropBoxData = $this.cropper('getCropBoxData');
129 // Re-render cropper.
130 $this.cropper('render');
132 // Get new data for cropper and calculate resize ratio.
133 var canvasDataNew = $this.cropper('getCanvasData');
135 if (canvasDataOld.width !== 0) {
136 ratio = canvasDataNew.width / canvasDataOld.width;
139 // Set new data for crop box.
140 $.each(cropBoxData, function (index, value) {
141 cropBoxData[index] = value * ratio;
143 $this.cropper('setCropBoxData', cropBoxData);
145 Drupal.imageWidgetCrop.updateHardLimits($this);
146 Drupal.imageWidgetCrop.checkSoftLimits($this);
147 Drupal.imageWidgetCrop.updateCropSummaries($this);
154 // Correctly updating messages of summaries.
155 Drupal.imageWidgetCrop.updateAllCropSummaries();
159 * Get ratio data and determine if an available ratio or free crop.
161 * @param {Object} $element - Element to initialize cropper on its children.
163 Drupal.imageWidgetCrop.getRatio = function ($element) {
164 var ratio = $element.data('ratio');
167 if ((regex.exec(ratio)) !== null) {
168 var int = ratio.split(":");
169 if ($.isArray(int) && ($.isNumeric(int[0]) && $.isNumeric(int[1]))) {
170 return int[0] / int[1];
182 * Initialize cropper on an element.
184 * @param {Object} $element - Element to initialize cropper on.
185 * @param {number} ratio - The ratio of the image.
187 Drupal.imageWidgetCrop.initializeCropper = function ($element, ratio) {
189 var $values = $element.siblings(cropperValuesSelector);
191 // Calculate minimal height for cropper container (minimal width is 200).
192 var minDelta = ($element.data('original-width') / 200);
193 cropperOptions['minContainerHeight'] = $element.data('original-height') / minDelta;
195 var options = cropperOptions;
196 var delta = $element.data('original-height') / $element.prop('naturalHeight');
198 // If 'Show default crop' is checked show crop box.
199 options.autoCrop = drupalSettings['crop_default'];
201 if (parseInt($values.find('.crop-applied').val()) === 1) {
203 x: Math.round(parseInt($values.find('.crop-x').val()) / delta),
204 y: Math.round(parseInt($values.find('.crop-y').val()) / delta),
205 width: Math.round(parseInt($values.find('.crop-width').val()) / delta),
206 height: Math.round(parseInt($values.find('.crop-height').val()) / delta),
211 options.autoCrop = true;
214 // React on crop move and check soft limits.
215 options.cropmove = function (e) {
216 Drupal.imageWidgetCrop.checkSoftLimits($(this));
220 options.aspectRatio = ratio;
222 $element.cropper(options);
224 // Hard and soft limits we need to check for fist time when cropper
225 // finished it initialization.
226 $element.on('built.cropper', function (e) {
228 Drupal.imageWidgetCrop.updateHardLimits($this);
229 Drupal.imageWidgetCrop.checkSoftLimits($this);
232 // If 'Show default crop' is checked apply default crop.
233 if (drupalSettings['crop_default']) {
234 var dataDefault = $element.cropper('getData');
235 // Calculate delta between original and thumbnail images.
236 var deltaDefault = $element.data('original-height') / $element.prop('naturalHeight');
238 * All data returned by cropper plugin multiple with delta in order to get
239 * proper crop sizes for original image.
241 Drupal.imageWidgetCrop.updateCropValues($values, dataDefault, deltaDefault);
242 Drupal.imageWidgetCrop.updateCropSummaries($element);
247 * Update crop values in hidden inputs.
249 * @param {Object} $element - Cropper values selector.
250 * @param {Array} $data - Cropper data.
251 * @param {number} $delta - Delta between original and thumbnail images.
253 Drupal.imageWidgetCrop.updateCropValues = function ($element, $data, $delta) {
254 $element.find('.crop-x').val(Math.round($data.x * $delta));
255 $element.find('.crop-y').val(Math.round($data.y * $delta));
256 $element.find('.crop-width').val(Math.round($data.width * $delta));
257 $element.find('.crop-height').val(Math.round($data.height * $delta));
258 $element.find('.crop-applied').val(1);
262 * Converts horizontal and vertical dimensions to canvas dimensions.
264 * @param {Object} $element - Crop element.
265 * @param {Number} x - horizontal dimension in image space.
266 * @param {Number} y - vertical dimension in image space.
268 Drupal.imageWidgetCrop.toCanvasDimensions = function ($element, x, y) {
269 var imageData = $element.data('cropper').getImageData();
271 width: imageData.width * (x / $element.data('original-width')),
272 height: imageData.height * (y / $element.data('original-height'))
277 * Converts horizontal and vertical dimensions to image dimensions.
279 * @param {Object} $element - Crop element.
280 * @param {Number} x - horizontal dimension in canvas space.
281 * @param {Number} y - vertical dimension in canvas space.
283 Drupal.imageWidgetCrop.toImageDimensions = function ($element, x, y) {
284 var imageData = $element.data('cropper').getImageData();
286 width: x * ($element.data('original-width') / imageData.width),
287 height: y * ($element.data('original-height') / imageData.height)
292 * Update hard limits for given element.
294 * @param {Object} $element - Crop element.
296 Drupal.imageWidgetCrop.updateHardLimits = function ($element) {
297 var cropName = $element.data('name');
299 // Check first that we have configuration for this crop.
300 if (!drupalSettings.image_widget_crop.hasOwnProperty(cropName)) {
304 var cropConfig = drupalSettings.image_widget_crop[cropName];
305 var cropper = $element.data('cropper');
306 var options = cropper.options;
308 // Limits works in canvas so we need to convert dimensions.
309 var converted = Drupal.imageWidgetCrop.toCanvasDimensions($element, cropConfig.hard_limit.width, cropConfig.hard_limit.height);
310 options.minCropBoxWidth = converted.width;
311 options.minCropBoxHeight = converted.height;
313 // After updating the options we need to limit crop box.
314 cropper.limitCropBox(true, false);
318 * Check soft limit for given crop element.
320 * @param {Object} $element - Crop element.
322 Drupal.imageWidgetCrop.checkSoftLimits = function ($element) {
323 var cropName = $element.data('name');
325 // Check first that we have configuration for this crop.
326 if (!drupalSettings.image_widget_crop.hasOwnProperty(cropName)) {
330 var cropConfig = drupalSettings.image_widget_crop[cropName];
332 var minSoftCropBox = {
333 'width': Number(cropConfig.soft_limit.width) || 0,
334 'height': Number(cropConfig.soft_limit.height) || 0
337 // We do comparison in image dimensions so lets convert first.
338 var cropBoxData = $element.cropper('getCropBoxData');
339 var converted = Drupal.imageWidgetCrop.toImageDimensions($element, cropBoxData.width, cropBoxData.height);
341 var dimensions = ['width', 'height'];
343 for (var i = 0; i < dimensions.length; ++i) {
344 // @todo - setting up soft limit status in data attribute is not ideal
345 // but current architecture is like that. When we convert to proper
346 // one imageWidgetCrop object per crop widget we will be able to fix
347 // this also. @see https://www.drupal.org/node/2660788.
348 var softLimitReached = $element.data(dimensions[i] + '-soft-limit-reached');
350 if (converted[dimensions[i]] < minSoftCropBox[dimensions[i]]) {
351 if (!softLimitReached) {
352 softLimitReached = true;
353 Drupal.imageWidgetCrop.softLimitChanged($element, dimensions[i], softLimitReached);
356 else if (softLimitReached) {
357 softLimitReached = false;
358 Drupal.imageWidgetCrop.softLimitChanged($element, dimensions[i], softLimitReached);
364 * React on soft limit change.
366 * @param {Object} $element - Crop element.
367 * @param {boolean} newSoftLimitState - new soft imit state, true if it
370 Drupal.imageWidgetCrop.softLimitChanged = function ($element, dimension, newSoftLimitState) {
371 var $cropperWrapper = $element.siblings('.cropper-container');
372 if (newSoftLimitState) {
373 $cropperWrapper.addClass('cropper--' + dimension + '-soft-limit-reached');
376 $cropperWrapper.removeClass('cropper--' + dimension + '-soft-limit-reached');
379 // @todo - use temporary storage while we are waiting for [#2660788].
380 $element.data(dimension + '-soft-limit-reached', newSoftLimitState);
382 Drupal.imageWidgetCrop.updateSingleCropSummary($element);
386 * Initialize cropper on all children of an element.
388 * @param {Object} $element - Element to initialize cropper on its children.
390 Drupal.imageWidgetCrop.initializeCropperOnChildren = function ($element) {
391 var visibleCropper = $element.find(cropperSelector + ':visible');
392 var ratio = Drupal.imageWidgetCrop.getRatio($(visibleCropper));
393 Drupal.imageWidgetCrop.initializeCropper($(visibleCropper), ratio);
397 * Update single crop summary of an element.
399 * @param {Object} $element - The element cropping on which has been changed.
401 Drupal.imageWidgetCrop.updateSingleCropSummary = function ($element) {
402 var $values = $element.siblings(cropperValuesSelector);
403 var croppingApplied = parseInt($values.find('.crop-applied').val());
404 var summaryMessages = [];
406 $element.closest('details').drupalSetSummary(function (context) {
407 if (croppingApplied === 1) {
408 summaryMessages.push(Drupal.t('Cropping applied.'));
411 if ($element.data('height-soft-limit-reached') || $element.data('width-soft-limit-reached')) {
412 summaryMessages.push(Drupal.t('Soft limit reached.'));
415 return summaryMessages.join('<br>');
420 * Update common crop summary of an element.
422 * @param {Object} $element - The element cropping on which has been changed.
424 Drupal.imageWidgetCrop.updateCommonCropSummary = function ($element) {
425 var croppingApplied = parseInt($element.find('.crop-applied[value="1"]').length);
426 var wrapperText = Drupal.t('Crop image');
427 if (croppingApplied) {
428 wrapperText = Drupal.t('Crop image (cropping applied)');
430 $element.find(cropWrapperSummarySelector).text(wrapperText);
434 * Update crop summaries after cropping cas been set or reset.
436 * @param {Object} $element - The element cropping on which has been changed.
438 Drupal.imageWidgetCrop.updateCropSummaries = function ($element) {
439 var $details = $element.closest('details' + cropWrapperSelector);
440 Drupal.imageWidgetCrop.updateSingleCropSummary($element);
441 Drupal.imageWidgetCrop.updateCommonCropSummary($details);
445 * Update crop summaries of all elements.
447 Drupal.imageWidgetCrop.updateAllCropSummaries = function () {
448 var $croppers = $(cropperSelector);
449 $croppers.each(function () {
450 Drupal.imageWidgetCrop.updateSingleCropSummary($(this));
452 var $cropWrappers = $(cropWrapperSelector);
453 $cropWrappers.each(function () {
454 Drupal.imageWidgetCrop.updateCommonCropSummary($(this));
459 * Reset cropping for an element.
461 * @param {Object} $element - The element to reset cropping on.
463 Drupal.imageWidgetCrop.reset = function ($element) {
464 var $valuesDefault = $element.siblings(cropperValuesSelector);
465 var options = cropperOptions;
466 // If 'Show default crop' is not checked re-initialize cropper.
467 if (!drupalSettings['crop_default']) {
468 $element.cropper('destroy');
469 options.autoCrop = false;
470 $element.cropper(options);
471 $valuesDefault.find('.crop-applied').val(0);
472 $valuesDefault.find('.crop-x').val('');
473 $valuesDefault.find('.crop-y').val('');
474 $valuesDefault.find('.crop-width').val('');
475 $valuesDefault.find('.crop-height').val('');
479 $element.cropper('reset').cropper('options', options);
480 var dataDefault = $element.cropper('getData');
481 // Calculate delta between original and thumbnail images.
482 var deltaDefault = $element.data('original-height') / $element.prop('naturalHeight');
484 * All data returned by cropper plugin multiple with delta in order to get
485 * proper crop sizes for original image.
487 Drupal.imageWidgetCrop.updateCropValues($valuesDefault, dataDefault, deltaDefault);
489 Drupal.imageWidgetCrop.updateCropSummaries($element);
492 Drupal.behaviors.imageWidgetCrop = {
493 attach: function (context) {
494 Drupal.imageWidgetCrop.initialize(context);
495 Drupal.imageWidgetCrop.updateAllCropSummaries();
499 }(jQuery, Drupal, drupalSettings));