Updated all the contrib modules to their latest versions.
[yaffs-website] / web / modules / contrib / image_widget_crop / js / ImageWidgetCropType.js
1 /**
2  * @file
3  * Defines the behaviors needed for cropper integration.
4  */
5
6 (function ($, Drupal) {
7   'use strict';
8
9   /**
10    * @class Drupal.ImageWidgetCropType
11    *
12    * @param {Drupal.ImageWidgetCrop} instance
13    *   The main ImageWidgetCrop instance that created this one.
14    *
15    * @param {HTMLElement|jQuery} element
16    *   The wrapper element.
17    */
18   Drupal.ImageWidgetCropType = function (instance, element) {
19
20     /**
21      * The ImageWidgetCrop instance responsible for creating this type.
22      *
23      * @type {Drupal.ImageWidgetCrop}
24      */
25     this.instance = instance;
26
27     /**
28      * The Cropper plugin wrapper element.
29      *
30      * @type {jQuery}
31      */
32     this.$cropperWrapper = $();
33
34     /**
35      * The wrapper element.
36      *
37      * @type {jQuery}
38      */
39     this.$wrapper = $(element);
40
41     /**
42      * The table element, if any.
43      *
44      * @type {jQuery}
45      */
46     this.$table = this.$wrapper.find(this.selectors.table);
47
48     /**
49      * The image element.
50      *
51      * @type {jQuery}
52      */
53     this.$image = this.$wrapper.find(this.selectors.image);
54
55     /**
56      * The reset element.
57      *
58      * @type {jQuery}
59      */
60     this.$reset = this.$wrapper.find(this.selectors.reset);
61
62     /**
63      * @type {Cropper}
64      */
65     this.cropper = null;
66
67     /**
68      * Flag indicating whether this instance is enabled.
69      *
70      * @type {Boolean}
71      */
72     this.enabled = true;
73
74     /**
75      * The hard limit of the crop.
76      *
77      * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
78      */
79     this.hardLimit = {
80       height: null,
81       width: null,
82       reached: {
83         height: false,
84         width: false
85       }
86     };
87
88     /**
89      * The unique identifier for this ImageWidgetCrop type.
90      *
91      * @type {String}
92      */
93     this.id = null;
94
95     /**
96      * Flag indicating whether the instance has been initialized.
97      *
98      * @type {Boolean}
99      */
100     this.initialized = false;
101
102     /**
103      * An object of recorded setInterval instances.
104      *
105      * @type {Object.<Number, jQuery>}
106      */
107     this.intervals = {};
108
109     /**
110      * The delta ratio of image based on its natural dimensions.
111      *
112      * @type {Number}
113      */
114     this.naturalDelta = null;
115
116     /**
117      * The natural height of the image.
118      *
119      * @type {Number}
120      */
121     this.naturalHeight = null;
122
123     /**
124      * The natural width of the image.
125      *
126      * @type {Number}
127      */
128     this.naturalWidth = null;
129
130     /**
131      * The original height of the image.
132      *
133      * @type {Number}
134      */
135     this.originalHeight = 0;
136
137     /**
138      * The original width of the image.
139      *
140      * @type {Number}
141      */
142     this.originalWidth = 0;
143
144     /**
145      * The current Cropper options.
146      *
147      * @type {Cropper.options}
148      */
149     this.options = {};
150
151     /**
152      * Flag indicating whether to show the default crop.
153      *
154      * @type {Boolean}
155      */
156     this.showDefaultCrop = true;
157
158     /**
159      * Flag indicating whether to show the default crop.
160      *
161      * @type {Boolean}
162      */
163     this.isRequired = false;
164
165     /**
166      * The soft limit of the crop.
167      *
168      * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
169      */
170     this.softLimit = {
171       height: null,
172       width: null,
173       reached: {
174         height: false,
175         width: false
176       }
177     };
178
179     /**
180      * The numeric representation of a ratio.
181      *
182      * @type {Number}
183      */
184     this.ratio = NaN;
185
186     /**
187      * The value elements.
188      *
189      * @type {Object.<String, jQuery>}
190      */
191     this.values = {
192       applied: this.$wrapper.find(this.selectors.values.applied),
193       height: this.$wrapper.find(this.selectors.values.height),
194       width: this.$wrapper.find(this.selectors.values.width),
195       x: this.$wrapper.find(this.selectors.values.x),
196       y: this.$wrapper.find(this.selectors.values.y)
197     };
198
199     /**
200      * Flag indicating whether the instance is currently visible.
201      *
202      * @type {Boolean}
203      */
204     this.visible = false;
205
206     // Initialize the instance.
207     this.init();
208   };
209
210   /**
211    * The prefix used for all Image Widget Crop data attributes.
212    *
213    * @type {RegExp}
214    */
215   Drupal.ImageWidgetCropType.prototype.dataPrefix = /^drupalIwc/;
216
217   /**
218    * Default options to pass to the Cropper plugin.
219    *
220    * @type {Object}
221    */
222   Drupal.ImageWidgetCropType.prototype.defaultOptions = {
223     autoCropArea: 1,
224     background: false,
225     responsive: false,
226     viewMode: 1,
227     zoomable: false
228   };
229
230   /**
231    * The selectors used to identify elements for this module.
232    *
233    * @type {Object}
234    */
235   Drupal.ImageWidgetCropType.prototype.selectors = {
236     image: '[data-drupal-iwc=image]',
237     reset: '[data-drupal-iwc=reset]',
238     table: '[data-drupal-iwc=table]',
239     values: {
240       applied: '[data-drupal-iwc-value=applied]',
241       height: '[data-drupal-iwc-value=height]',
242       width: '[data-drupal-iwc-value=width]',
243       x: '[data-drupal-iwc-value=x]',
244       y: '[data-drupal-iwc-value=y]'
245     }
246   };
247
248   /**
249    * The "built" event handler for the Cropper plugin.
250    */
251   Drupal.ImageWidgetCropType.prototype.built = function () {
252     this.$cropperWrapper = this.$wrapper.find('.cropper-container');
253     this.updateHardLimits();
254     this.updateSoftLimits();
255   };
256
257   /**
258    * The "cropend" event handler for the Cropper plugin.
259    */
260   Drupal.ImageWidgetCropType.prototype.cropEnd = function () {
261     // Immediately return if there is no cropper instance (for whatever reason).
262     if (!this.cropper) {
263       return;
264     }
265
266     // Retrieve the cropper data.
267     var data = this.cropper.getData();
268
269     // Ensure the applied state is enabled.
270     data.applied = 1;
271
272     // Data returned by Cropper plugin should be multiplied with delta in order
273     // to get the proper crop sizes for the original image.
274     this.setValues(data, this.naturalDelta);
275
276     // Trigger summary updates.
277     this.$wrapper.trigger('summaryUpdated');
278   };
279
280   /**
281    * The "cropmove" event handler for the Cropper plugin.
282    */
283   Drupal.ImageWidgetCropType.prototype.cropMove = function () {
284     this.built();
285   };
286
287   /**
288    * Destroys this instance.
289    */
290   Drupal.ImageWidgetCropType.prototype.destroy = function () {
291     this.destroyCropper();
292
293     this.$image.off('.iwc');
294     this.$reset.off('.iwc');
295
296     // Clear any intervals that were set.
297     for (var interval in this.intervals) {
298       if (this.intervals.hasOwnProperty(interval)) {
299         clearInterval(interval);
300         delete this.intervals[interval];
301       }
302     }
303   };
304
305   /**
306    * Destroys the Cropper plugin instance.
307    */
308   Drupal.ImageWidgetCropType.prototype.destroyCropper = function () {
309     this.$image.off('.iwc.cropper');
310     if (this.cropper) {
311       this.cropper.destroy();
312       this.cropper = null;
313     }
314   };
315
316   /**
317    * Disables this instance.
318    */
319   Drupal.ImageWidgetCropType.prototype.disable = function () {
320     if (this.cropper) {
321       this.cropper.disable();
322     }
323     this.$table.removeClass('responsive-enabled--opened');
324   };
325
326   /**
327    * Enables this instance.
328    */
329   Drupal.ImageWidgetCropType.prototype.enable = function () {
330     if (this.cropper) {
331       this.cropper.enable();
332     }
333     this.$table.addClass('responsive-enabled--opened');
334   };
335
336   /**
337    * Retrieves a crop value.
338    *
339    * @param {'applied'|'height'|'width'|'x'|'y'} name
340    *   The name of the crop value to retrieve.
341    * @param {Number} [delta]
342    *   The delta amount to divide value by, if any.
343    *
344    * @return {Number}
345    *   The crop value.
346    */
347   Drupal.ImageWidgetCropType.prototype.getValue = function (name, delta) {
348     var value = 0;
349     if (this.values[name] && this.values[name][0]) {
350       value = parseInt(this.values[name][0].value, 10) || 0;
351     }
352     return name !== 'applied' && value && delta ? Math.round(value / delta) : value;
353   };
354
355   /**
356    * Retrieves all crop values.
357    *
358    * @param {Number} [delta]
359    *   The delta amount to divide value by, if any.
360    *
361    * @return {{applied: Number, height: Number, width: Number, x: Number, y: Number}}
362    *   The crop value key/value pairs.
363    */
364   Drupal.ImageWidgetCropType.prototype.getValues = function (delta) {
365     var values = {};
366     for (var name in this.values) {
367       if (this.values.hasOwnProperty(name)) {
368         values[name] = this.getValue(name, delta);
369       }
370     }
371     return values;
372   };
373
374   /**
375    * Initializes the instance.
376    */
377   Drupal.ImageWidgetCropType.prototype.init = function () {
378     // Immediately return if already initialized.
379     if (this.initialized) {
380       return;
381     }
382
383     // Set the default options.
384     this.options = $.extend({}, this.defaultOptions);
385     this.isRequired = this.$wrapper.data('drupalIwcRequired');
386
387     // Extend this instance with data from the wrapper.
388     var data = this.$wrapper.data();
389     for (var i in data) {
390       if (data.hasOwnProperty(i) && this.dataPrefix.test(i)) {
391         // Remove Drupal + module prefix and lowercase the first letter.
392         var prop = i.replace(this.dataPrefix, '');
393         prop = prop.charAt(0).toLowerCase() + prop.slice(1);
394
395         // Check if data attribute exists on this object.
396         if (prop && this.hasOwnProperty(prop)) {
397           var value = data[i];
398
399           // Parse the ratio value.
400           if (prop === 'ratio') {
401             value = this.parseRatio(value);
402           }
403           this[prop] = typeof value === 'object' ? $.extend(true, {}, this[prop], value) : value;
404         }
405       }
406     }
407
408     // Bind necessary events.
409     this.$image
410       .on('visible.iwc', function () {
411         this.visible = true;
412         this.naturalHeight = parseInt(this.$image.prop('naturalHeight'), 10);
413         this.naturalWidth = parseInt(this.$image.prop('naturalWidth'), 10);
414         // Calculate delta between original and thumbnail images.
415         this.naturalDelta = this.originalHeight && this.naturalHeight ? this.originalHeight / this.naturalHeight : null;
416       }.bind(this))
417       // Only initialize the cropper plugin once.
418       .one('visible.iwc', this.initializeCropper.bind(this))
419       .on('hidden.iwc', function () {
420         this.visible = false;
421       }.bind(this));
422
423     this.$reset
424       .on('click.iwc', this.reset.bind(this));
425
426     // Star polling visibility of the image that should be able to be cropped.
427     this.pollVisibility(this.$image);
428
429     // Bind the drupalSetSummary callback.
430     this.$wrapper.drupalSetSummary(this.updateSummary.bind(this));
431
432     // Trigger the initial summaryUpdate event.
433     this.$wrapper.trigger('summaryUpdated');
434     var isIE = /*@cc_on!@*/false || !!document.documentMode;
435     if (isIE) {
436       var $image = this.$image;
437       $('.image-data__crop-wrapper > summary').on('click', function () {
438         setTimeout(function () {$image.trigger('visible.iwc')}, 100);
439       });
440     }
441   };
442
443   /**
444    * Initializes the Cropper plugin.
445    */
446   Drupal.ImageWidgetCropType.prototype.initializeCropper = function () {
447     // Calculate minimal height for cropper container (minimal width is 200).
448     var minDelta = (this.originalWidth / 200);
449     this.options.minContainerHeight = this.originalHeight / minDelta;
450
451     // Only autoCrop if 'Show default crop' is checked.
452     this.options.autoCrop = this.showDefaultCrop;
453
454     // Set aspect ratio.
455     this.options.aspectRatio = this.ratio;
456
457     // Initialize data.
458     var values = this.getValues(this.naturalDelta);
459     this.options.data = this.options.data || {};
460     if (values.applied) {
461       // Remove the "applied" value as it has no meaning in Cropper.
462       delete values.applied;
463
464       // Merge in the values.
465       this.options.data = $.extend(true, this.options.data, values);
466
467       // Enforce autoCrop if there's currently a crop applied.
468       this.options.autoCrop = true;
469     }
470
471     this.options.data.scaleX = 1;
472     this.options.data.scaleY = 1;
473
474     this.$image
475       .on('built.iwc.cropper', this.built.bind(this))
476       .on('cropend.iwc.cropper', this.cropEnd.bind(this))
477       .on('cropmove.iwc.cropper', this.cropMove.bind(this))
478       .cropper(this.options);
479
480     this.cropper = this.$image.data('cropper');
481     this.options = this.cropper.options;
482
483     // If "Show default crop" is checked apply default crop.
484     if (this.showDefaultCrop) {
485       // All data returned by cropper plugin multiple with delta in order to get
486       // proper crop sizes for original image.
487       this.setValue(this.$image.cropper('getData'), this.naturalDelta);
488       this.$wrapper.trigger('summaryUpdated');
489     }
490   };
491
492   /**
493    * Creates a poll that checks visibility of an item.
494    *
495    * @param {HTMLElement|jQuery} element
496    *   The element to poll.
497    *
498    * Replace once vertical tabs have proper events ?
499    * When following issue are fixed @see https://www.drupal.org/node/2653570.
500    */
501   Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) {
502     var $element = $(element);
503
504     // Immediately return if there's no element.
505     if (!$element[0]) {
506       return;
507     }
508
509     var isElementVisible = function (el) {
510       var rect = el.getBoundingClientRect();
511       var vWidth = window.innerWidth || document.documentElement.clientWidth;
512       var vHeight = window.innerHeight || document.documentElement.clientHeight;
513
514       // Immediately Return false if it's not in the viewport.
515       if (rect.right < 0 || rect.bottom < 0 || rect.left > vWidth || rect.top > vHeight) {
516         return false;
517       }
518
519       // Return true if any of its four corners are visible.
520       var efp = function (x, y) {
521         return document.elementFromPoint(x, y);
522       };
523       return (
524         el.contains(efp(rect.left, rect.top))
525         || el.contains(efp(rect.right, rect.top))
526         || el.contains(efp(rect.right, rect.bottom))
527         || el.contains(efp(rect.left, rect.bottom))
528       );
529     };
530
531     var value = null;
532     var interval = setInterval(function () {
533       var visible = isElementVisible($element[0]);
534       if (value !== visible) {
535         $element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc');
536       }
537     }, 250);
538     this.intervals[interval] = $element;
539   };
540
541   /**
542    * Parses a ration value into a numeric one.
543    *
544    * @param {String} ratio
545    *   A string representation of the ratio.
546    *
547    * @return {Number.<float>|NaN}
548    *   The numeric representation of the ratio.
549    */
550   Drupal.ImageWidgetCropType.prototype.parseRatio = function (ratio) {
551     if (ratio && /:/.test(ratio)) {
552       var parts = ratio.split(':');
553       var num1 = parseInt(parts[0], 10);
554       var num2 = parseInt(parts[1], 10);
555       return num1 / num2;
556     }
557     return parseFloat(ratio);
558   };
559
560   /**
561    * Reset cropping for an element.
562    *
563    * @param {Event} e
564    *   The event object.
565    */
566   Drupal.ImageWidgetCropType.prototype.reset = function (e) {
567     if (!this.cropper) {
568       return;
569     }
570
571     if (e instanceof Event || e instanceof $.Event) {
572       e.preventDefault();
573       e.stopPropagation();
574     }
575
576     this.options = $.extend({}, this.cropper.options, this.defaultOptions);
577
578     var delta = null;
579
580     // Retrieve all current values and zero (0) them out.
581     var values = this.getValues();
582     for (var name in values) {
583       if (values.hasOwnProperty(name)) {
584         values[name] = 0;
585       }
586     }
587
588     // If 'Show default crop' is not checked just re-initialize the cropper.
589     if (!this.showDefaultCrop) {
590       this.destroyCropper();
591       this.initializeCropper();
592     }
593     // Reset cropper to the original values.
594     else {
595       this.cropper.reset();
596       this.cropper.options = this.options;
597
598       // Set the delta.
599       delta = this.naturalDelta;
600
601       // Merge in the original cropper values.
602       values = $.extend(values, this.cropper.getData());
603     }
604
605     this.setValues(values, delta);
606     this.$wrapper.trigger('summaryUpdated');
607   };
608
609   /**
610    * The "resize" event handler proxied from the main instance.
611    *
612    * @see Drupal.ImageWidgetCrop.prototype.resize
613    */
614   Drupal.ImageWidgetCropType.prototype.resize = function () {
615     // Immediately return if currently not visible.
616     if (!this.visible) {
617       return;
618     }
619
620     // Get previous data for cropper.
621     var canvasDataOld = this.$image.cropper('getCanvasData');
622     var cropBoxData = this.$image.cropper('getCropBoxData');
623
624     // Re-render cropper.
625     this.$image.cropper('render');
626
627     // Get new data for cropper and calculate resize ratio.
628     var canvasDataNew = this.$image.cropper('getCanvasData');
629     var ratio = 1;
630     if (canvasDataOld.width !== 0) {
631       ratio = canvasDataNew.width / canvasDataOld.width;
632     }
633
634     // Set new data for crop box.
635     $.each(cropBoxData, function (index, value) {
636       cropBoxData[index] = value * ratio;
637     });
638     this.$image.cropper('setCropBoxData', cropBoxData);
639
640     this.updateHardLimits();
641     this.updateSoftLimits();
642     this.$wrapper.trigger('summaryUpdated');
643   };
644
645   /**
646    * Sets a single crop value.
647    *
648    * @param {'applied'|'height'|'width'|'x'|'y'} name
649    *   The name of the crop value to set.
650    * @param {Number} value
651    *   The value to set.
652    * @param {Number} [delta]
653    *   A delta to modify the value with.
654    */
655   Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) {
656     if (!this.values.hasOwnProperty(name) || !this.values[name][0]) {
657       return;
658     }
659     value = value ? parseFloat(value) : 0;
660     if (delta && name !== 'applied') {
661       value = Math.round(value * delta);
662     }
663     this.values[name][0].value = value;
664     this.values[name].trigger('change.iwc, input.iwc');
665   };
666
667   /**
668    * Sets multiple crop values.
669    *
670    * @param {{applied: Number, height: Number, width: Number, x: Number, y: Number}} obj
671    *   An object of key/value pairs of values to set.
672    * @param {Number} [delta]
673    *   A delta to modify the value with.
674    */
675   Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) {
676     for (var name in obj) {
677       if (!obj.hasOwnProperty(name)) {
678         continue;
679       }
680       this.setValue(name, obj[name], delta);
681     }
682   };
683
684   /**
685    * Converts horizontal and vertical dimensions to canvas dimensions.
686    *
687    * @param {Number} x - horizontal dimension in image space.
688    * @param {Number} y - vertical dimension in image space.
689    */
690   Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) {
691     var imageData = this.cropper.getImageData();
692     return {
693       width: imageData.width * (x / this.originalWidth),
694       height: imageData.height * (y / this.originalHeight)
695     }
696   };
697
698   /**
699    * Converts horizontal and vertical dimensions to image dimensions.
700    *
701    * @param {Number} x - horizontal dimension in canvas space.
702    * @param {Number} y - vertical dimension in canvas space.
703    */
704   Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) {
705     var imageData = this.cropper.getImageData();
706     return {
707       width: x * (this.originalWidth / imageData.width),
708       height: y * (this.originalHeight / imageData.height)
709     }
710   };
711
712   /**
713    * Update hard limits.
714    */
715   Drupal.ImageWidgetCropType.prototype.updateHardLimits = function () {
716     // Immediately return if there is no cropper plugin instance or hard limits.
717     if (!this.cropper || !this.hardLimit.width || !this.hardLimit.height) {
718       return;
719     }
720
721     var options = this.cropper.options;
722
723     // Limits works in canvas so we need to convert dimensions.
724     var converted = this.toCanvasDimensions(this.hardLimit.width, this.hardLimit.height);
725     options.minCropBoxWidth = converted.width;
726     options.minCropBoxHeight = converted.height;
727
728     // After updating the options we need to limit crop box.
729     this.cropper.limitCropBox(true, false);
730   };
731
732   /**
733    * Update soft limits.
734    */
735   Drupal.ImageWidgetCropType.prototype.updateSoftLimits = function () {
736     // Immediately return if there is no cropper plugin instance or soft limits.
737     if (!this.cropper || !this.softLimit.width || !this.softLimit.height) {
738       return;
739     }
740
741     // We do comparison in image dimensions so lets convert first.
742     var cropBoxData = this.cropper.getCropBoxData();
743     var converted = this.toImageDimensions(cropBoxData.width, cropBoxData.height);
744
745     var dimensions = ['width', 'height'];
746     for (var i = 0, l = dimensions.length; i < l; i++) {
747       var dimension = dimensions[i];
748       if (converted[dimension] < this.softLimit[dimension]) {
749         if (!this.softLimit.reached[dimension]) {
750           this.softLimit.reached[dimension] = true;
751         }
752       }
753       else if (this.softLimit.reached[dimension]) {
754         this.softLimit.reached[dimension] = false;
755       }
756       this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]);
757     }
758     this.$wrapper.trigger('summaryUpdated');
759   };
760
761   /**
762    * Updates the summary of the wrapper.
763    */
764   Drupal.ImageWidgetCropType.prototype.updateSummary = function () {
765     var summary = [];
766     if (this.getValue('applied')) {
767       summary.push(Drupal.t('Cropping applied.'));
768     }
769     if (this.softLimit.reached.height || this.softLimit.reached.width) {
770       summary.push(Drupal.t('Soft limit reached.'));
771     }
772     return summary.join('<br>');
773   };
774
775     /**
776      * Override Theme function for a vertical tabs.
777      *
778      * @param {object} settings
779      *   An object with the following keys:
780      * @param {string} settings.title
781      *   The name of the tab.
782      *
783      * @return {object}
784      *   This function has to return an object with at least these keys:
785      *   - item: The root tab jQuery element
786      *   - link: The anchor tag that acts as the clickable area of the tab
787      *       (jQuery version)
788      *   - summary: The jQuery element that contains the tab summary
789      */
790   Drupal.theme.verticalTab = function (settings) {
791       var tab = {};
792       this.isRequired = settings.details.data('drupalIwcRequired');
793       tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>').append(tab.link = $('<a href="#"></a>').append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title)).append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>')));
794
795       // If those Crop type is required add attributes.
796       if (this.isRequired) {
797         tab.title.addClass('js-form-required form-required');
798       }
799
800       return tab;
801   };
802
803 }(jQuery, Drupal));