Version 1
[yaffs-website] / web / modules / contrib / image_widget_crop / js / imageWidgetCrop.js
1 /**
2  * @file
3  * Defines the behaviors needed for cropper integration.
4  */
5
6 (function ($, Drupal, drupalSettings) {
7   'use strict';
8
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 = {
21     background: false,
22     zoomable: false,
23     viewMode: 1,
24     autoCropArea: 1,
25     responsive: false,
26     // Callback function, fires when crop is applied.
27     cropend: function (e) {
28       var $this = $(this);
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');
33       /*
34        * All data returned by cropper plugin multiple with delta in order to get
35        * proper crop sizes for original image.
36        */
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);
43     }
44   };
45
46   Drupal.imageWidgetCrop = {};
47
48   /**
49    * Initialize cropper on the ImageWidgetCrop widget.
50    *
51    * @param {Object} context - Element to initialize cropper on.
52    */
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);
59
60     /*
61      * Cropper initialization on click events on vertical tabs and details
62      * summaries (for smaller screens).
63      */
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);
69       }
70       var ratio = Drupal.imageWidgetCrop.getRatio($cropper);
71       Drupal.imageWidgetCrop.initializeCropper($cropper, ratio);
72     });
73
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')) {
80           evt.preventDefault();
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());
87           } else {
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());
95           }
96           evt.stopImmediatePropagation();
97         }
98         // If detailsWrapper is visible hide it.
99         else {
100           $(this).parent().removeAttr('open');
101           $(table).removeClass('responsive-enabled--opened');
102           $(this).parent().find(detailsWrapper).hide();
103         }
104       }
105     });
106
107     $reset.on('click', function (e) {
108       e.preventDefault();
109       var $element = $(this).siblings(cropperSelector);
110       Drupal.imageWidgetCrop.reset($element);
111       return false;
112     });
113
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 () {
123             var $this = $(this);
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');
128
129               // Re-render cropper.
130               $this.cropper('render');
131
132               // Get new data for cropper and calculate resize ratio.
133               var canvasDataNew = $this.cropper('getCanvasData');
134               var ratio = 1;
135               if (canvasDataOld.width !== 0) {
136                 ratio = canvasDataNew.width / canvasDataOld.width;
137               }
138
139               // Set new data for crop box.
140               $.each(cropBoxData, function (index, value) {
141                 cropBoxData[index] = value * ratio;
142               });
143               $this.cropper('setCropBoxData', cropBoxData);
144
145               Drupal.imageWidgetCrop.updateHardLimits($this);
146               Drupal.imageWidgetCrop.checkSoftLimits($this);
147               Drupal.imageWidgetCrop.updateCropSummaries($this);
148             }
149           });
150         });
151       });
152     });
153
154     // Correctly updating messages of summaries.
155     Drupal.imageWidgetCrop.updateAllCropSummaries();
156   };
157
158   /**
159    * Get ratio data and determine if an available ratio or free crop.
160    *
161    * @param {Object} $element - Element to initialize cropper on its children.
162    */
163   Drupal.imageWidgetCrop.getRatio = function ($element) {
164     var ratio = $element.data('ratio');
165     var regex = /:/;
166
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];
171       }
172       else {
173         return "NaN";
174       }
175     }
176     else {
177       return ratio;
178     }
179   };
180
181   /**
182    * Initialize cropper on an element.
183    *
184    * @param {Object} $element - Element to initialize cropper on.
185    * @param {number} ratio - The ratio of the image.
186    */
187   Drupal.imageWidgetCrop.initializeCropper = function ($element, ratio) {
188     var data = null;
189     var $values = $element.siblings(cropperValuesSelector);
190
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;
194
195     var options = cropperOptions;
196     var delta = $element.data('original-height') / $element.prop('naturalHeight');
197
198     // If 'Show default crop' is checked show crop box.
199     options.autoCrop = drupalSettings['crop_default'];
200
201     if (parseInt($values.find('.crop-applied').val()) === 1) {
202       data = {
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),
207         rotate: 0,
208         scaleX: 1,
209         scaleY: 1
210       };
211       options.autoCrop = true;
212     }
213
214     // React on crop move and check soft limits.
215     options.cropmove = function (e) {
216       Drupal.imageWidgetCrop.checkSoftLimits($(this));
217     };
218
219     options.data = data;
220     options.aspectRatio = ratio;
221
222     $element.cropper(options);
223
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) {
227       var $this = $(this);
228       Drupal.imageWidgetCrop.updateHardLimits($this);
229       Drupal.imageWidgetCrop.checkSoftLimits($this);
230     });
231
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');
237       /*
238        * All data returned by cropper plugin multiple with delta in order to get
239        * proper crop sizes for original image.
240        */
241       Drupal.imageWidgetCrop.updateCropValues($values, dataDefault, deltaDefault);
242       Drupal.imageWidgetCrop.updateCropSummaries($element);
243     }
244   };
245
246   /**
247    * Update crop values in hidden inputs.
248    *
249    * @param {Object} $element - Cropper values selector.
250    * @param {Array} $data - Cropper data.
251    * @param {number} $delta - Delta between original and thumbnail images.
252    */
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);
259   };
260
261   /**
262    * Converts horizontal and vertical dimensions to canvas dimensions.
263    *
264    * @param {Object} $element - Crop element.
265    * @param {Number} x - horizontal dimension in image space.
266    * @param {Number} y - vertical dimension in image space.
267    */
268   Drupal.imageWidgetCrop.toCanvasDimensions = function ($element, x, y) {
269     var imageData = $element.data('cropper').getImageData();
270     return {
271       width: imageData.width * (x / $element.data('original-width')),
272       height: imageData.height * (y / $element.data('original-height'))
273     }
274   };
275
276   /**
277    * Converts horizontal and vertical dimensions to image dimensions.
278    *
279    * @param {Object} $element - Crop element.
280    * @param {Number} x - horizontal dimension in canvas space.
281    * @param {Number} y - vertical dimension in canvas space.
282    */
283   Drupal.imageWidgetCrop.toImageDimensions = function ($element, x, y) {
284     var imageData = $element.data('cropper').getImageData();
285     return {
286       width: x * ($element.data('original-width') / imageData.width),
287       height: y * ($element.data('original-height') / imageData.height)
288     }
289   };
290
291   /**
292    * Update hard limits for given element.
293    *
294    * @param {Object} $element - Crop element.
295    */
296   Drupal.imageWidgetCrop.updateHardLimits = function ($element) {
297     var cropName = $element.data('name');
298
299     // Check first that we have configuration for this crop.
300     if (!drupalSettings.image_widget_crop.hasOwnProperty(cropName)) {
301       return;
302     }
303
304     var cropConfig = drupalSettings.image_widget_crop[cropName];
305     var cropper = $element.data('cropper');
306     var options = cropper.options;
307
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;
312
313     // After updating the options we need to limit crop box.
314     cropper.limitCropBox(true, false);
315   };
316
317   /**
318    * Check soft limit for given crop element.
319    *
320    * @param {Object} $element - Crop element.
321    */
322   Drupal.imageWidgetCrop.checkSoftLimits = function ($element) {
323     var cropName = $element.data('name');
324
325     // Check first that we have configuration for this crop.
326     if (!drupalSettings.image_widget_crop.hasOwnProperty(cropName)) {
327       return;
328     }
329
330     var cropConfig = drupalSettings.image_widget_crop[cropName];
331
332     var minSoftCropBox = {
333       'width': Number(cropConfig.soft_limit.width) || 0,
334       'height': Number(cropConfig.soft_limit.height) || 0
335     };
336
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);
340
341     var dimensions = ['width', 'height'];
342
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');
349
350       if (converted[dimensions[i]] < minSoftCropBox[dimensions[i]]) {
351         if (!softLimitReached) {
352           softLimitReached = true;
353           Drupal.imageWidgetCrop.softLimitChanged($element, dimensions[i], softLimitReached);
354         }
355       }
356       else if (softLimitReached) {
357         softLimitReached = false;
358         Drupal.imageWidgetCrop.softLimitChanged($element, dimensions[i], softLimitReached);
359       }
360     }
361   };
362
363   /**
364    * React on soft limit change.
365    *
366    * @param {Object} $element - Crop element.
367    * @param {boolean} newSoftLimitState - new soft imit state, true if it
368    *   reached, or false.
369    */
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');
374     }
375     else {
376       $cropperWrapper.removeClass('cropper--' + dimension + '-soft-limit-reached');
377     }
378
379     // @todo - use temporary storage while we are waiting for [#2660788].
380     $element.data(dimension + '-soft-limit-reached', newSoftLimitState);
381
382     Drupal.imageWidgetCrop.updateSingleCropSummary($element);
383   };
384
385   /**
386    * Initialize cropper on all children of an element.
387    *
388    * @param {Object} $element - Element to initialize cropper on its children.
389    */
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);
394   };
395
396   /**
397    * Update single crop summary of an element.
398    *
399    * @param {Object} $element - The element cropping on which has been changed.
400    */
401   Drupal.imageWidgetCrop.updateSingleCropSummary = function ($element) {
402     var $values = $element.siblings(cropperValuesSelector);
403     var croppingApplied = parseInt($values.find('.crop-applied').val());
404     var summaryMessages = [];
405
406     $element.closest('details').drupalSetSummary(function (context) {
407       if (croppingApplied === 1) {
408         summaryMessages.push(Drupal.t('Cropping applied.'));
409       }
410
411       if ($element.data('height-soft-limit-reached') || $element.data('width-soft-limit-reached')) {
412         summaryMessages.push(Drupal.t('Soft limit reached.'));
413       }
414
415       return summaryMessages.join('<br>');
416     });
417   };
418
419   /**
420    * Update common crop summary of an element.
421    *
422    * @param {Object} $element - The element cropping on which has been changed.
423    */
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)');
429     }
430     $element.find(cropWrapperSummarySelector).text(wrapperText);
431   };
432
433   /**
434    * Update crop summaries after cropping cas been set or reset.
435    *
436    * @param {Object} $element - The element cropping on which has been changed.
437    */
438   Drupal.imageWidgetCrop.updateCropSummaries = function ($element) {
439     var $details = $element.closest('details' + cropWrapperSelector);
440     Drupal.imageWidgetCrop.updateSingleCropSummary($element);
441     Drupal.imageWidgetCrop.updateCommonCropSummary($details);
442   };
443
444   /**
445    * Update crop summaries of all elements.
446    */
447   Drupal.imageWidgetCrop.updateAllCropSummaries = function () {
448     var $croppers = $(cropperSelector);
449     $croppers.each(function () {
450       Drupal.imageWidgetCrop.updateSingleCropSummary($(this));
451     });
452     var $cropWrappers = $(cropWrapperSelector);
453     $cropWrappers.each(function () {
454       Drupal.imageWidgetCrop.updateCommonCropSummary($(this));
455     });
456   };
457
458   /**
459    * Reset cropping for an element.
460    *
461    * @param {Object} $element - The element to reset cropping on.
462    */
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('');
476     }
477     else {
478       // Reset cropper.
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');
483       /*
484        * All data returned by cropper plugin multiple with delta in order to get
485        * proper crop sizes for original image.
486        */
487       Drupal.imageWidgetCrop.updateCropValues($valuesDefault, dataDefault, deltaDefault);
488     }
489     Drupal.imageWidgetCrop.updateCropSummaries($element);
490   };
491
492   Drupal.behaviors.imageWidgetCrop = {
493     attach: function (context) {
494       Drupal.imageWidgetCrop.initialize(context);
495       Drupal.imageWidgetCrop.updateAllCropSummaries();
496     }
497   };
498
499 }(jQuery, Drupal, drupalSettings));