Updated to Drupal 8.5. Core Media not yet in use.
[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      * The soft limit of the crop.
160      *
161      * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
162      */
163     this.softLimit = {
164       height: null,
165       width: null,
166       reached: {
167         height: false,
168         width: false
169       }
170     };
171
172     /**
173      * The numeric representation of a ratio.
174      *
175      * @type {Number}
176      */
177     this.ratio = NaN;
178
179     /**
180      * The value elements.
181      *
182      * @type {Object.<String, jQuery>}
183      */
184     this.values = {
185       applied: this.$wrapper.find(this.selectors.values.applied),
186       height: this.$wrapper.find(this.selectors.values.height),
187       width: this.$wrapper.find(this.selectors.values.width),
188       x: this.$wrapper.find(this.selectors.values.x),
189       y: this.$wrapper.find(this.selectors.values.y)
190     };
191
192     /**
193      * Flag indicating whether the instance is currently visible.
194      *
195      * @type {Boolean}
196      */
197     this.visible = false;
198
199     // Initialize the instance.
200     this.init();
201   };
202
203   /**
204    * The prefix used for all Image Widget Crop data attributes.
205    *
206    * @type {RegExp}
207    */
208   Drupal.ImageWidgetCropType.prototype.dataPrefix = /^drupalIwc/;
209
210   /**
211    * Default options to pass to the Cropper plugin.
212    *
213    * @type {Object}
214    */
215   Drupal.ImageWidgetCropType.prototype.defaultOptions = {
216     autoCropArea: 1,
217     background: false,
218     responsive: false,
219     viewMode: 1,
220     zoomable: false
221   };
222
223   /**
224    * The selectors used to identify elements for this module.
225    *
226    * @type {Object}
227    */
228   Drupal.ImageWidgetCropType.prototype.selectors = {
229     image: '[data-drupal-iwc=image]',
230     reset: '[data-drupal-iwc=reset]',
231     table: '[data-drupal-iwc=table]',
232     values: {
233       applied: '[data-drupal-iwc-value=applied]',
234       height: '[data-drupal-iwc-value=height]',
235       width: '[data-drupal-iwc-value=width]',
236       x: '[data-drupal-iwc-value=x]',
237       y: '[data-drupal-iwc-value=y]'
238     }
239   };
240
241   /**
242    * The "built" event handler for the Cropper plugin.
243    */
244   Drupal.ImageWidgetCropType.prototype.built = function () {
245     this.$cropperWrapper = this.$wrapper.find('.cropper-container');
246     this.updateHardLimits();
247     this.updateSoftLimits();
248   };
249
250   /**
251    * The "cropend" event handler for the Cropper plugin.
252    */
253   Drupal.ImageWidgetCropType.prototype.cropEnd = function () {
254     // Immediately return if there is no cropper instance (for whatever reason).
255     if (!this.cropper) {
256       return;
257     }
258
259     // Retrieve the cropper data.
260     var data = this.cropper.getData();
261
262     // Ensure the applied state is enabled.
263     data.applied = 1;
264
265     // Data returned by Cropper plugin should be multiplied with delta in order
266     // to get the proper crop sizes for the original image.
267     this.setValues(data, this.naturalDelta);
268
269     // Trigger summary updates.
270     this.$wrapper.trigger('summaryUpdated');
271   };
272
273   /**
274    * The "cropmove" event handler for the Cropper plugin.
275    */
276   Drupal.ImageWidgetCropType.prototype.cropMove = function () {
277     this.updateSoftLimits();
278   };
279
280   /**
281    * Destroys this instance.
282    */
283   Drupal.ImageWidgetCropType.prototype.destroy = function () {
284     this.destroyCropper();
285
286     this.$image.off('.iwc');
287     this.$reset.off('.iwc');
288
289     // Clear any intervals that were set.
290     for (var interval in this.intervals) {
291       if (this.intervals.hasOwnProperty(interval)) {
292         clearInterval(interval);
293         delete this.intervals[interval];
294       }
295     }
296   };
297
298   /**
299    * Destroys the Cropper plugin instance.
300    */
301   Drupal.ImageWidgetCropType.prototype.destroyCropper = function () {
302     this.$image.off('.iwc.cropper');
303     if (this.cropper) {
304       this.cropper.destroy();
305       this.cropper = null;
306     }
307   };
308
309   /**
310    * Disables this instance.
311    */
312   Drupal.ImageWidgetCropType.prototype.disable = function () {
313     if (this.cropper) {
314       this.cropper.disable();
315     }
316     this.$table.removeClass('responsive-enabled--opened');
317   };
318
319   /**
320    * Enables this instance.
321    */
322   Drupal.ImageWidgetCropType.prototype.enable = function () {
323     if (this.cropper) {
324       this.cropper.enable();
325     }
326     this.$table.addClass('responsive-enabled--opened');
327   };
328
329   /**
330    * Retrieves a crop value.
331    *
332    * @param {'applied'|'height'|'width'|'x'|'y'} name
333    *   The name of the crop value to retrieve.
334    * @param {Number} [delta]
335    *   The delta amount to divide value by, if any.
336    *
337    * @return {Number}
338    *   The crop value.
339    */
340   Drupal.ImageWidgetCropType.prototype.getValue = function (name, delta) {
341     var value = 0;
342     if (this.values[name] && this.values[name][0]) {
343       value = parseInt(this.values[name][0].value, 10) || 0;
344     }
345     return name !== 'applied' && value && delta ? Math.round(value / delta) : value;
346   };
347
348   /**
349    * Retrieves all crop values.
350    *
351    * @param {Number} [delta]
352    *   The delta amount to divide value by, if any.
353    *
354    * @return {{applied: Number, height: Number, width: Number, x: Number, y: Number}}
355    *   The crop value key/value pairs.
356    */
357   Drupal.ImageWidgetCropType.prototype.getValues = function (delta) {
358     var values = {};
359     for (var name in this.values) {
360       if (this.values.hasOwnProperty(name)) {
361         values[name] = this.getValue(name, delta);
362       }
363     }
364     return values;
365   };
366
367   /**
368    * Initializes the instance.
369    */
370   Drupal.ImageWidgetCropType.prototype.init = function () {
371     // Immediately return if already initialized.
372     if (this.initialized) {
373       return;
374     }
375
376     // Set the default options.
377     this.options = $.extend({}, this.defaultOptions);
378
379     // Extend this instance with data from the wrapper.
380     var data = this.$wrapper.data();
381     for (var i in data) {
382       if (data.hasOwnProperty(i) && this.dataPrefix.test(i)) {
383         // Remove Drupal + module prefix and lowercase the first letter.
384         var prop = i.replace(this.dataPrefix, '');
385         prop = prop.charAt(0).toLowerCase() + prop.slice(1);
386
387         // Check if data attribute exists on this object.
388         if (prop && this.hasOwnProperty(prop)) {
389           var value = data[i];
390
391           // Parse the ratio value.
392           if (prop === 'ratio') {
393             value = this.parseRatio(value);
394           }
395           this[prop] = typeof value === 'object' ? $.extend(true, {}, this[prop], value) : value;
396         }
397       }
398     }
399
400     // Bind necessary events.
401     this.$image
402       .on('visible.iwc', function () {
403         this.visible = true;
404         this.naturalHeight = parseInt(this.$image.prop('naturalHeight'), 10);
405         this.naturalWidth = parseInt(this.$image.prop('naturalWidth'), 10);
406         // Calculate delta between original and thumbnail images.
407         this.naturalDelta = this.originalHeight && this.naturalHeight ? this.originalHeight / this.naturalHeight : null;
408       }.bind(this))
409       // Only initialize the cropper plugin once.
410       .one('visible.iwc', this.initializeCropper.bind(this))
411       .on('hidden.iwc', function () {
412         this.visible = false;
413       }.bind(this));
414
415     this.$reset
416       .on('click.iwc', this.reset.bind(this));
417
418     // Star polling visibility of the image that should be able to be cropped.
419     this.pollVisibility(this.$image);
420
421     // Bind the drupalSetSummary callback.
422     this.$wrapper.drupalSetSummary(this.updateSummary.bind(this));
423
424     // Trigger the initial summaryUpdate event.
425     this.$wrapper.trigger('summaryUpdated');
426     var isIE = /*@cc_on!@*/false || !!document.documentMode;
427     if (isIE) {
428       var $image = this.$image;
429       $('.image-data__crop-wrapper > summary').on('click', function () {
430         setTimeout(function () {$image.trigger('visible.iwc')}, 100);
431       });
432     }
433   };
434
435   /**
436    * Initializes the Cropper plugin.
437    */
438   Drupal.ImageWidgetCropType.prototype.initializeCropper = function () {
439     // Calculate minimal height for cropper container (minimal width is 200).
440     var minDelta = (this.originalWidth / 200);
441     this.options.minContainerHeight = this.originalHeight / minDelta;
442
443     // Only autoCrop if 'Show default crop' is checked.
444     this.options.autoCrop = this.showDefaultCrop;
445
446     // Set aspect ratio.
447     this.options.aspectRatio = this.ratio;
448
449     // Initialize data.
450     var values = this.getValues(this.naturalDelta);
451     this.options.data = this.options.data || {};
452     if (values.applied) {
453       // Remove the "applied" value as it has no meaning in Cropper.
454       delete values.applied;
455
456       // Merge in the values.
457       this.options.data = $.extend(true, this.options.data, values);
458
459       // Enforce autoCrop if there's currently a crop applied.
460       this.options.autoCrop = true;
461     }
462
463     this.options.data.rotate = 0;
464     this.options.data.scaleX = 1;
465     this.options.data.scaleY = 1;
466
467     this.$image
468       .on('built.iwc.cropper', this.built.bind(this))
469       .on('cropend.iwc.cropper', this.cropEnd.bind(this))
470       .on('cropmove.iwc.cropper', this.cropMove.bind(this))
471       .cropper(this.options);
472
473     this.cropper = this.$image.data('cropper');
474     this.options = this.cropper.options;
475
476     // If "Show default crop" is checked apply default crop.
477     if (this.showDefaultCrop) {
478       // All data returned by cropper plugin multiple with delta in order to get
479       // proper crop sizes for original image.
480       this.setValue(this.$image.cropper('getData'), this.naturalDelta);
481       this.$wrapper.trigger('summaryUpdated');
482     }
483   };
484
485   /**
486    * Creates a poll that checks visibility of an item.
487    *
488    * @param {HTMLElement|jQuery} element
489    *   The element to poll.
490    *
491    * Replace once vertical tabs have proper events ?
492    * When following issue are fixed @see https://www.drupal.org/node/2653570.
493    */
494   Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) {
495     var $element = $(element);
496
497     // Immediately return if there's no element.
498     if (!$element[0]) {
499       return;
500     }
501
502     var isElementVisible = function (el) {
503       var rect = el.getBoundingClientRect();
504       var vWidth = window.innerWidth || document.documentElement.clientWidth;
505       var vHeight = window.innerHeight || document.documentElement.clientHeight;
506
507       // Immediately Return false if it's not in the viewport.
508       if (rect.right < 0 || rect.bottom < 0 || rect.left > vWidth || rect.top > vHeight) {
509         return false;
510       }
511
512       // Return true if any of its four corners are visible.
513       var efp = function (x, y) {
514         return document.elementFromPoint(x, y);
515       };
516       return (
517         el.contains(efp(rect.left, rect.top))
518         || el.contains(efp(rect.right, rect.top))
519         || el.contains(efp(rect.right, rect.bottom))
520         || el.contains(efp(rect.left, rect.bottom))
521       );
522     };
523
524     var value = null;
525     var interval = setInterval(function () {
526       var visible = isElementVisible($element[0]);
527       if (value !== visible) {
528         $element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc');
529       }
530     }, 250);
531     this.intervals[interval] = $element;
532   };
533
534   /**
535    * Parses a ration value into a numeric one.
536    *
537    * @param {String} ratio
538    *   A string representation of the ratio.
539    *
540    * @return {Number.<float>|NaN}
541    *   The numeric representation of the ratio.
542    */
543   Drupal.ImageWidgetCropType.prototype.parseRatio = function (ratio) {
544     if (ratio && /:/.test(ratio)) {
545       var parts = ratio.split(':');
546       var num1 = parseInt(parts[0], 10);
547       var num2 = parseInt(parts[1], 10);
548       return num1 / num2;
549     }
550     return parseFloat(ratio);
551   };
552
553   /**
554    * Reset cropping for an element.
555    *
556    * @param {Event} e
557    *   The event object.
558    */
559   Drupal.ImageWidgetCropType.prototype.reset = function (e) {
560     if (!this.cropper) {
561       return;
562     }
563
564     if (e instanceof Event || e instanceof $.Event) {
565       e.preventDefault();
566       e.stopPropagation();
567     }
568
569     this.options = $.extend({}, this.cropper.options, this.defaultOptions);
570
571     var delta = null;
572
573     // Retrieve all current values and zero (0) them out.
574     var values = this.getValues();
575     for (var name in values) {
576       if (values.hasOwnProperty(name)) {
577         values[name] = 0;
578       }
579     }
580
581     // If 'Show default crop' is not checked just re-initialize the cropper.
582     if (!this.showDefaultCrop) {
583       this.destroyCropper();
584       this.initializeCropper();
585     }
586     // Reset cropper to the original values.
587     else {
588       this.cropper.reset();
589       this.cropper.options = this.options;
590
591       // Set the delta.
592       delta = this.naturalDelta;
593
594       // Merge in the original cropper values.
595       values = $.extend(values, this.cropper.getData());
596     }
597
598     this.setValues(values, delta);
599     this.$wrapper.trigger('summaryUpdated');
600   };
601
602   /**
603    * The "resize" event handler proxied from the main instance.
604    *
605    * @see Drupal.ImageWidgetCrop.prototype.resize
606    */
607   Drupal.ImageWidgetCropType.prototype.resize = function () {
608     // Immediately return if currently not visible.
609     if (!this.visible) {
610       return;
611     }
612
613     // Get previous data for cropper.
614     var canvasDataOld = this.$image.cropper('getCanvasData');
615     var cropBoxData = this.$image.cropper('getCropBoxData');
616
617     // Re-render cropper.
618     this.$image.cropper('render');
619
620     // Get new data for cropper and calculate resize ratio.
621     var canvasDataNew = this.$image.cropper('getCanvasData');
622     var ratio = 1;
623     if (canvasDataOld.width !== 0) {
624       ratio = canvasDataNew.width / canvasDataOld.width;
625     }
626
627     // Set new data for crop box.
628     $.each(cropBoxData, function (index, value) {
629       cropBoxData[index] = value * ratio;
630     });
631     this.$image.cropper('setCropBoxData', cropBoxData);
632
633     this.updateHardLimits();
634     this.updateSoftLimits();
635     this.$wrapper.trigger('summaryUpdated');
636   };
637
638   /**
639    * Sets a single crop value.
640    *
641    * @param {'applied'|'height'|'width'|'x'|'y'} name
642    *   The name of the crop value to set.
643    * @param {Number} value
644    *   The value to set.
645    * @param {Number} [delta]
646    *   A delta to modify the value with.
647    */
648   Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) {
649     if (!this.values.hasOwnProperty(name) || !this.values[name][0]) {
650       return;
651     }
652     value = value ? parseFloat(value) : 0;
653     if (delta && name !== 'applied') {
654       value = Math.round(value * delta);
655     }
656     this.values[name][0].value = value;
657     this.values[name].trigger('change.iwc, input.iwc');
658   };
659
660   /**
661    * Sets multiple crop values.
662    *
663    * @param {{applied: Number, height: Number, width: Number, x: Number, y: Number}} obj
664    *   An object of key/value pairs of values to set.
665    * @param {Number} [delta]
666    *   A delta to modify the value with.
667    */
668   Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) {
669     for (var name in obj) {
670       if (!obj.hasOwnProperty(name)) {
671         continue;
672       }
673       this.setValue(name, obj[name], delta);
674     }
675   };
676
677   /**
678    * Converts horizontal and vertical dimensions to canvas dimensions.
679    *
680    * @param {Number} x - horizontal dimension in image space.
681    * @param {Number} y - vertical dimension in image space.
682    */
683   Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) {
684     var imageData = this.cropper.getImageData();
685     return {
686       width: imageData.width * (x / this.originalWidth),
687       height: imageData.height * (y / this.originalHeight)
688     }
689   };
690
691   /**
692    * Converts horizontal and vertical dimensions to image dimensions.
693    *
694    * @param {Number} x - horizontal dimension in canvas space.
695    * @param {Number} y - vertical dimension in canvas space.
696    */
697   Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) {
698     var imageData = this.cropper.getImageData();
699     return {
700       width: x * (this.originalWidth / imageData.width),
701       height: y * (this.originalHeight / imageData.height)
702     }
703   };
704
705   /**
706    * Update hard limits.
707    */
708   Drupal.ImageWidgetCropType.prototype.updateHardLimits = function () {
709     // Immediately return if there is no cropper plugin instance or hard limits.
710     if (!this.cropper || !this.hardLimit.width || !this.hardLimit.height) {
711       return;
712     }
713
714     var options = this.cropper.options;
715
716     // Limits works in canvas so we need to convert dimensions.
717     var converted = this.toCanvasDimensions(this.hardLimit.width, this.hardLimit.height);
718     options.minCropBoxWidth = converted.width;
719     options.minCropBoxHeight = converted.height;
720
721     // After updating the options we need to limit crop box.
722     this.cropper.limitCropBox(true, false);
723   };
724
725   /**
726    * Update soft limits.
727    */
728   Drupal.ImageWidgetCropType.prototype.updateSoftLimits = function () {
729     // Immediately return if there is no cropper plugin instance or soft limits.
730     if (!this.cropper || !this.softLimit.width || !this.softLimit.height) {
731       return;
732     }
733
734     // We do comparison in image dimensions so lets convert first.
735     var cropBoxData = this.cropper.getCropBoxData();
736     var converted = this.toImageDimensions(cropBoxData.width, cropBoxData.height);
737
738     var dimensions = ['width', 'height'];
739     for (var i = 0, l = dimensions.length; i < l; i++) {
740       var dimension = dimensions[i];
741       if (converted[dimension] < this.softLimit[dimension]) {
742         if (!this.softLimit.reached[dimension]) {
743           this.softLimit.reached[dimension] = true;
744         }
745       }
746       else if (this.softLimit.reached[dimension]) {
747         this.softLimit.reached[dimension] = false;
748       }
749       this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]);
750     }
751     this.$wrapper.trigger('summaryUpdated');
752   };
753
754   /**
755    * Updates the summary of the wrapper.
756    */
757   Drupal.ImageWidgetCropType.prototype.updateSummary = function () {
758     var summary = [];
759     if (this.getValue('applied')) {
760       summary.push(Drupal.t('Cropping applied.'));
761     }
762     if (this.softLimit.reached.height || this.softLimit.reached.width) {
763       summary.push(Drupal.t('Soft limit reached.'));
764     }
765     return summary.join('<br>');
766   };
767
768 }(jQuery, Drupal));