3 * Defines the behaviors needed for cropper integration.
6 (function ($, Drupal) {
10 * @class Drupal.ImageWidgetCropType
12 * @param {Drupal.ImageWidgetCrop} instance
13 * The main ImageWidgetCrop instance that created this one.
15 * @param {HTMLElement|jQuery} element
16 * The wrapper element.
18 Drupal.ImageWidgetCropType = function (instance, element) {
21 * The ImageWidgetCrop instance responsible for creating this type.
23 * @type {Drupal.ImageWidgetCrop}
25 this.instance = instance;
28 * The Cropper plugin wrapper element.
32 this.$cropperWrapper = $();
35 * The wrapper element.
39 this.$wrapper = $(element);
42 * The table element, if any.
46 this.$table = this.$wrapper.find(this.selectors.table);
53 this.$image = this.$wrapper.find(this.selectors.image);
60 this.$reset = this.$wrapper.find(this.selectors.reset);
68 * Flag indicating whether this instance is enabled.
75 * The hard limit of the crop.
77 * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
89 * The unique identifier for this ImageWidgetCrop type.
96 * Flag indicating whether the instance has been initialized.
100 this.initialized = false;
103 * An object of recorded setInterval instances.
105 * @type {Object.<Number, jQuery>}
110 * The delta ratio of image based on its natural dimensions.
114 this.naturalDelta = null;
117 * The natural height of the image.
121 this.naturalHeight = null;
124 * The natural width of the image.
128 this.naturalWidth = null;
131 * The original height of the image.
135 this.originalHeight = 0;
138 * The original width of the image.
142 this.originalWidth = 0;
145 * The current Cropper options.
147 * @type {Cropper.options}
152 * Flag indicating whether to show the default crop.
156 this.showDefaultCrop = true;
159 * The soft limit of the crop.
161 * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
173 * The numeric representation of a ratio.
180 * The value elements.
182 * @type {Object.<String, jQuery>}
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)
193 * Flag indicating whether the instance is currently visible.
197 this.visible = false;
199 // Initialize the instance.
204 * The prefix used for all Image Widget Crop data attributes.
208 Drupal.ImageWidgetCropType.prototype.dataPrefix = /^drupalIwc/;
211 * Default options to pass to the Cropper plugin.
215 Drupal.ImageWidgetCropType.prototype.defaultOptions = {
224 * The selectors used to identify elements for this module.
228 Drupal.ImageWidgetCropType.prototype.selectors = {
229 image: '[data-drupal-iwc=image]',
230 reset: '[data-drupal-iwc=reset]',
231 table: '[data-drupal-iwc=table]',
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]'
242 * The "built" event handler for the Cropper plugin.
244 Drupal.ImageWidgetCropType.prototype.built = function () {
245 this.$cropperWrapper = this.$wrapper.find('.cropper-container');
246 this.updateHardLimits();
247 this.updateSoftLimits();
251 * The "cropend" event handler for the Cropper plugin.
253 Drupal.ImageWidgetCropType.prototype.cropEnd = function () {
254 // Immediately return if there is no cropper instance (for whatever reason).
259 // Retrieve the cropper data.
260 var data = this.cropper.getData();
262 // Ensure the applied state is enabled.
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);
269 // Trigger summary updates.
270 this.$wrapper.trigger('summaryUpdated');
274 * The "cropmove" event handler for the Cropper plugin.
276 Drupal.ImageWidgetCropType.prototype.cropMove = function () {
277 this.updateSoftLimits();
281 * Destroys this instance.
283 Drupal.ImageWidgetCropType.prototype.destroy = function () {
284 this.destroyCropper();
286 this.$image.off('.iwc');
287 this.$reset.off('.iwc');
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];
299 * Destroys the Cropper plugin instance.
301 Drupal.ImageWidgetCropType.prototype.destroyCropper = function () {
302 this.$image.off('.iwc.cropper');
304 this.cropper.destroy();
310 * Disables this instance.
312 Drupal.ImageWidgetCropType.prototype.disable = function () {
314 this.cropper.disable();
316 this.$table.removeClass('responsive-enabled--opened');
320 * Enables this instance.
322 Drupal.ImageWidgetCropType.prototype.enable = function () {
324 this.cropper.enable();
326 this.$table.addClass('responsive-enabled--opened');
330 * Retrieves a crop value.
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.
340 Drupal.ImageWidgetCropType.prototype.getValue = function (name, delta) {
342 if (this.values[name] && this.values[name][0]) {
343 value = parseInt(this.values[name][0].value, 10) || 0;
345 return name !== 'applied' && value && delta ? Math.round(value / delta) : value;
349 * Retrieves all crop values.
351 * @param {Number} [delta]
352 * The delta amount to divide value by, if any.
354 * @return {{applied: Number, height: Number, width: Number, x: Number, y: Number}}
355 * The crop value key/value pairs.
357 Drupal.ImageWidgetCropType.prototype.getValues = function (delta) {
359 for (var name in this.values) {
360 if (this.values.hasOwnProperty(name)) {
361 values[name] = this.getValue(name, delta);
368 * Initializes the instance.
370 Drupal.ImageWidgetCropType.prototype.init = function () {
371 // Immediately return if already initialized.
372 if (this.initialized) {
376 // Set the default options.
377 this.options = $.extend({}, this.defaultOptions);
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);
387 // Check if data attribute exists on this object.
388 if (prop && this.hasOwnProperty(prop)) {
391 // Parse the ratio value.
392 if (prop === 'ratio') {
393 value = this.parseRatio(value);
395 this[prop] = typeof value === 'object' ? $.extend(true, {}, this[prop], value) : value;
400 // Bind necessary events.
402 .on('visible.iwc', function () {
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;
409 // Only initialize the cropper plugin once.
410 .one('visible.iwc', this.initializeCropper.bind(this))
411 .on('hidden.iwc', function () {
412 this.visible = false;
416 .on('click.iwc', this.reset.bind(this));
418 // Star polling visibility of the image that should be able to be cropped.
419 this.pollVisibility(this.$image);
421 // Bind the drupalSetSummary callback.
422 this.$wrapper.drupalSetSummary(this.updateSummary.bind(this));
424 // Trigger the initial summaryUpdate event.
425 this.$wrapper.trigger('summaryUpdated');
426 var isIE = /*@cc_on!@*/false || !!document.documentMode;
428 var $image = this.$image;
429 $('.image-data__crop-wrapper > summary').on('click', function () {
430 setTimeout(function () {$image.trigger('visible.iwc')}, 100);
436 * Initializes the Cropper plugin.
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;
443 // Only autoCrop if 'Show default crop' is checked.
444 this.options.autoCrop = this.showDefaultCrop;
447 this.options.aspectRatio = this.ratio;
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;
456 // Merge in the values.
457 this.options.data = $.extend(true, this.options.data, values);
459 // Enforce autoCrop if there's currently a crop applied.
460 this.options.autoCrop = true;
463 this.options.data.rotate = 0;
464 this.options.data.scaleX = 1;
465 this.options.data.scaleY = 1;
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);
473 this.cropper = this.$image.data('cropper');
474 this.options = this.cropper.options;
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');
486 * Creates a poll that checks visibility of an item.
488 * @param {HTMLElement|jQuery} element
489 * The element to poll.
491 * Replace once vertical tabs have proper events ?
492 * When following issue are fixed @see https://www.drupal.org/node/2653570.
494 Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) {
495 var $element = $(element);
497 // Immediately return if there's no element.
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;
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) {
512 // Return true if any of its four corners are visible.
513 var efp = function (x, y) {
514 return document.elementFromPoint(x, y);
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))
525 var interval = setInterval(function () {
526 var visible = isElementVisible($element[0]);
527 if (value !== visible) {
528 $element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc');
531 this.intervals[interval] = $element;
535 * Parses a ration value into a numeric one.
537 * @param {String} ratio
538 * A string representation of the ratio.
540 * @return {Number.<float>|NaN}
541 * The numeric representation of the ratio.
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);
550 return parseFloat(ratio);
554 * Reset cropping for an element.
559 Drupal.ImageWidgetCropType.prototype.reset = function (e) {
564 if (e instanceof Event || e instanceof $.Event) {
569 this.options = $.extend({}, this.cropper.options, this.defaultOptions);
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)) {
581 // If 'Show default crop' is not checked just re-initialize the cropper.
582 if (!this.showDefaultCrop) {
583 this.destroyCropper();
584 this.initializeCropper();
586 // Reset cropper to the original values.
588 this.cropper.reset();
589 this.cropper.options = this.options;
592 delta = this.naturalDelta;
594 // Merge in the original cropper values.
595 values = $.extend(values, this.cropper.getData());
598 this.setValues(values, delta);
599 this.$wrapper.trigger('summaryUpdated');
603 * The "resize" event handler proxied from the main instance.
605 * @see Drupal.ImageWidgetCrop.prototype.resize
607 Drupal.ImageWidgetCropType.prototype.resize = function () {
608 // Immediately return if currently not visible.
613 // Get previous data for cropper.
614 var canvasDataOld = this.$image.cropper('getCanvasData');
615 var cropBoxData = this.$image.cropper('getCropBoxData');
617 // Re-render cropper.
618 this.$image.cropper('render');
620 // Get new data for cropper and calculate resize ratio.
621 var canvasDataNew = this.$image.cropper('getCanvasData');
623 if (canvasDataOld.width !== 0) {
624 ratio = canvasDataNew.width / canvasDataOld.width;
627 // Set new data for crop box.
628 $.each(cropBoxData, function (index, value) {
629 cropBoxData[index] = value * ratio;
631 this.$image.cropper('setCropBoxData', cropBoxData);
633 this.updateHardLimits();
634 this.updateSoftLimits();
635 this.$wrapper.trigger('summaryUpdated');
639 * Sets a single crop value.
641 * @param {'applied'|'height'|'width'|'x'|'y'} name
642 * The name of the crop value to set.
643 * @param {Number} value
645 * @param {Number} [delta]
646 * A delta to modify the value with.
648 Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) {
649 if (!this.values.hasOwnProperty(name) || !this.values[name][0]) {
652 value = value ? parseFloat(value) : 0;
653 if (delta && name !== 'applied') {
654 value = Math.round(value * delta);
656 this.values[name][0].value = value;
657 this.values[name].trigger('change.iwc, input.iwc');
661 * Sets multiple crop values.
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.
668 Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) {
669 for (var name in obj) {
670 if (!obj.hasOwnProperty(name)) {
673 this.setValue(name, obj[name], delta);
678 * Converts horizontal and vertical dimensions to canvas dimensions.
680 * @param {Number} x - horizontal dimension in image space.
681 * @param {Number} y - vertical dimension in image space.
683 Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) {
684 var imageData = this.cropper.getImageData();
686 width: imageData.width * (x / this.originalWidth),
687 height: imageData.height * (y / this.originalHeight)
692 * Converts horizontal and vertical dimensions to image dimensions.
694 * @param {Number} x - horizontal dimension in canvas space.
695 * @param {Number} y - vertical dimension in canvas space.
697 Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) {
698 var imageData = this.cropper.getImageData();
700 width: x * (this.originalWidth / imageData.width),
701 height: y * (this.originalHeight / imageData.height)
706 * Update hard limits.
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) {
714 var options = this.cropper.options;
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;
721 // After updating the options we need to limit crop box.
722 this.cropper.limitCropBox(true, false);
726 * Update soft limits.
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) {
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);
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;
746 else if (this.softLimit.reached[dimension]) {
747 this.softLimit.reached[dimension] = false;
749 this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]);
751 this.$wrapper.trigger('summaryUpdated');
755 * Updates the summary of the wrapper.
757 Drupal.ImageWidgetCropType.prototype.updateSummary = function () {
759 if (this.getValue('applied')) {
760 summary.push(Drupal.t('Cropping applied.'));
762 if (this.softLimit.reached.height || this.softLimit.reached.width) {
763 summary.push(Drupal.t('Soft limit reached.'));
765 return summary.join('<br>');