image_widget_crop-8.x-2.x-dev/js/ImageWidgetCropType.js
js/ImageWidgetCropType.js
/**
* @file
* Defines the behaviors needed for cropper integration.
*/
(function ($, Drupal) {
'use strict';
/**
* @class Drupal.ImageWidgetCropType
*
* @param {Drupal.ImageWidgetCrop} instance
* The main ImageWidgetCrop instance that created this one.
*
* @param {HTMLElement|jQuery} element
* The wrapper element.
*/
Drupal.ImageWidgetCropType = function (instance, element) {
/**
* The ImageWidgetCrop instance responsible for creating this type.
*
* @type {Drupal.ImageWidgetCrop}
*/
this.instance = instance;
/**
* The Cropper plugin wrapper element.
*
* @type {jQuery}
*/
this.$cropperWrapper = $();
/**
* The wrapper element.
*
* @type {jQuery}
*/
this.$wrapper = $(element);
/**
* The table element, if any.
*
* @type {jQuery}
*/
this.$table = this.$wrapper.find(this.selectors.table);
/**
* The image element.
*
* @type {jQuery}
*/
this.$image = this.$wrapper.find(this.selectors.image);
/**
* The reset element.
*
* @type {jQuery}
*/
this.$reset = this.$wrapper.find(this.selectors.reset);
/**
* @type {Cropper}
*/
this.cropper = null;
/**
* Flag indicating whether this instance is enabled.
*
* @type {Boolean}
*/
this.enabled = true;
/**
* The hard limit of the crop.
*
* @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
*/
this.hardLimit = {
height: null,
width: null,
reached: {
height: false,
width: false
}
};
/**
* The unique identifier for this ImageWidgetCrop type.
*
* @type {String}
*/
this.id = null;
/**
* Flag indicating whether the instance has been initialized.
*
* @type {Boolean}
*/
this.initialized = false;
/**
* An object of recorded setInterval instances.
*
* @type {Object.<Number, jQuery>}
*/
this.intervals = {};
/**
* The delta ratio of image based on its natural dimensions.
*
* @type {Number}
*/
this.naturalDelta = null;
/**
* The natural height of the image.
*
* @type {Number}
*/
this.naturalHeight = null;
/**
* The natural width of the image.
*
* @type {Number}
*/
this.naturalWidth = null;
/**
* The original height of the image.
*
* @type {Number}
*/
this.originalHeight = 0;
/**
* The original width of the image.
*
* @type {Number}
*/
this.originalWidth = 0;
/**
* The current Cropper options.
*
* @type {Cropper.options}
*/
this.options = {};
/**
* Flag indicating whether to show the default crop.
*
* @type {Boolean}
*/
this.showDefaultCrop = true;
/**
* Flag indicating whether to show the default crop.
*
* @type {Boolean}
*/
this.isRequired = false;
/**
* The soft limit of the crop.
*
* @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
*/
this.softLimit = {
height: null,
width: null,
reached: {
height: false,
width: false
}
};
/**
* The numeric representation of a ratio.
*
* @type {Number}
*/
this.ratio = NaN;
/**
* The value elements.
*
* @type {Object.<String, jQuery>}
*/
this.values = {
applied: this.$wrapper.find(this.selectors.values.applied),
height: this.$wrapper.find(this.selectors.values.height),
width: this.$wrapper.find(this.selectors.values.width),
x: this.$wrapper.find(this.selectors.values.x),
y: this.$wrapper.find(this.selectors.values.y)
};
/**
* Flag indicating whether the instance is currently visible.
*
* @type {Boolean}
*/
this.visible = false;
// Initialize the instance.
this.init();
};
/**
* The prefix used for all Image Widget Crop data attributes.
*
* @type {RegExp}
*/
Drupal.ImageWidgetCropType.prototype.dataPrefix = /^drupalIwc/;
/**
* Default options to pass to the Cropper plugin.
*
* @type {Object}
*/
Drupal.ImageWidgetCropType.prototype.defaultOptions = {
autoCropArea: 1,
background: false,
responsive: false,
viewMode: 1,
zoomable: false
};
/**
* The selectors used to identify elements for this module.
*
* @type {Object}
*/
Drupal.ImageWidgetCropType.prototype.selectors = {
image: '[data-drupal-iwc=image]',
reset: '[data-drupal-iwc=reset]',
table: '[data-drupal-iwc=table]',
values: {
applied: '[data-drupal-iwc-value=applied]',
height: '[data-drupal-iwc-value=height]',
width: '[data-drupal-iwc-value=width]',
x: '[data-drupal-iwc-value=x]',
y: '[data-drupal-iwc-value=y]'
}
};
/**
* The "ready" event handler for the Cropper plugin.
*/
Drupal.ImageWidgetCropType.prototype.cropperReady = function () {
// Set crop limits.
this.built();
// Restore saved crop box data.
if (this.getValue('applied')) {
// Convert data.
var canvasData = this.cropper.getCanvasData();
var cropBoxData = this.getValues(this.originalHeight / canvasData.height);
cropBoxData.left = cropBoxData.x + canvasData.left;
cropBoxData.top = cropBoxData.y + canvasData.top;
// TEMP Bind height and width to max to avoid cropper.js bug.
var containerData = this.cropper.getContainerData();
var limited = this.options.viewMode === 1 || this.options.viewMode === 2;
var maxCropBoxWidth = Math.min(containerData.width, limited ? canvasData.width : containerData.width);
var maxCropBoxHeight = Math.min(containerData.height, limited ? canvasData.height : containerData.height);
if (this.ratio) {
if (maxCropBoxHeight * this.ratio > maxCropBoxWidth) {
maxCropBoxHeight = maxCropBoxWidth / this.ratio;
} else {
maxCropBoxWidth = maxCropBoxHeight * this.ratio;
}
}
cropBoxData.width = Math.min(cropBoxData.width, maxCropBoxWidth);
cropBoxData.height = Math.min(cropBoxData.height, maxCropBoxHeight);
// Restore data.
this.cropper.setCropBoxData(cropBoxData);
}
}
/**
* The "built" event handler for the Cropper plugin.
*/
Drupal.ImageWidgetCropType.prototype.built = function () {
this.$cropperWrapper = this.$wrapper.find('.cropper-container');
this.updateHardLimits();
this.updateSoftLimits();
};
/**
* The "cropend" event handler for the Cropper plugin.
*/
Drupal.ImageWidgetCropType.prototype.cropEnd = function () {
// Immediately return if there is no cropper instance (for whatever reason).
if (!this.cropper) {
return;
}
// Retrieve the cropper data.
var data = this.cropper.getData();
// Ensure the applied state is enabled.
data.applied = 1;
// Data returned by Cropper plugin should be multiplied with delta in order
// to get the proper crop sizes for the original image.
this.setValues(data, this.naturalDelta);
// Trigger summary updates.
this.$wrapper.trigger('summaryUpdated');
};
/**
* The "cropmove" event handler for the Cropper plugin.
*/
Drupal.ImageWidgetCropType.prototype.cropMove = function () {
this.built();
};
/**
* Destroys this instance.
*/
Drupal.ImageWidgetCropType.prototype.destroy = function () {
this.destroyCropper();
this.$image.off('.iwc');
this.$reset.off('.iwc');
// Clear any intervals that were set.
for (var interval in this.intervals) {
if (this.intervals.hasOwnProperty(interval)) {
clearInterval(interval);
delete this.intervals[interval];
}
}
};
/**
* Destroys the Cropper plugin instance.
*/
Drupal.ImageWidgetCropType.prototype.destroyCropper = function () {
this.$image.off('.iwc.cropper');
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
};
/**
* Disables this instance.
*/
Drupal.ImageWidgetCropType.prototype.disable = function () {
if (this.cropper) {
this.cropper.disable();
}
this.$table.removeClass('responsive-enabled--opened');
};
/**
* Enables this instance.
*/
Drupal.ImageWidgetCropType.prototype.enable = function () {
if (this.cropper) {
this.cropper.enable();
}
this.$table.addClass('responsive-enabled--opened');
};
/**
* Retrieves a crop value.
*
* @param {'applied'|'height'|'width'|'x'|'y'} name
* The name of the crop value to retrieve.
* @param {Number} [delta]
* The delta amount to divide value by, if any.
*
* @return {Number}
* The crop value.
*/
Drupal.ImageWidgetCropType.prototype.getValue = function (name, delta) {
var value = 0;
if (this.values[name] && this.values[name][0]) {
value = parseInt(this.values[name][0].value, 10) || 0;
}
return name !== 'applied' && value && delta ? Math.floor(value / delta) : value;
};
/**
* Retrieves all crop values.
*
* @param {Number} [delta]
* The delta amount to divide value by, if any.
*
* @return {{applied: Number, height: Number, width: Number, x: Number, y: Number}}
* The crop value key/value pairs.
*/
Drupal.ImageWidgetCropType.prototype.getValues = function (delta) {
var values = {};
for (var name in this.values) {
if (this.values.hasOwnProperty(name)) {
values[name] = this.getValue(name, delta);
}
}
return values;
};
/**
* Initializes the instance.
*/
Drupal.ImageWidgetCropType.prototype.init = function () {
// Immediately return if already initialized.
if (this.initialized) {
return;
}
// Set the default options.
this.options = $.extend({}, this.defaultOptions);
this.isRequired = this.$wrapper.data('drupalIwcRequired');
// Extend this instance with data from the wrapper.
var data = this.$wrapper.data();
for (var i in data) {
if (hasOwnProperty.call(data, i) && this.dataPrefix.test(i)) {
// Remove Drupal + module prefix and lowercase the first letter.
var prop = i.replace(this.dataPrefix, '');
prop = prop.charAt(0).toLowerCase() + prop.slice(1);
// Check if data attribute exists on this object.
if (prop && this.hasOwnProperty(prop)) {
var value = data[i];
// Parse the ratio value.
if (prop === 'ratio') {
value = this.parseRatio(value);
}
this[prop] = typeof value === 'object' ? $.extend(true, {}, this[prop], value) : value;
}
}
}
// Bind necessary events.
this.$image
.on('visible.iwc', function () {
this.visible = true;
this.naturalHeight = parseInt(this.$image.prop('naturalHeight'), 10);
this.naturalWidth = parseInt(this.$image.prop('naturalWidth'), 10);
// Calculate delta between original and thumbnail images.
this.naturalDelta = this.originalHeight && this.naturalHeight ? this.originalHeight / this.naturalHeight : null;
}.bind(this))
// Only initialize the cropper plugin once.
.one('visible.iwc', this.initializeCropper.bind(this))
.on('hidden.iwc', function () {
this.visible = false;
}.bind(this));
this.$reset
.on('click.iwc', this.reset.bind(this));
// Star polling visibility of the image that should be able to be cropped.
this.pollVisibility(this.$image);
// Bind the drupalSetSummary callback.
this.$wrapper.drupalSetSummary(this.updateSummary.bind(this));
// Trigger the initial summaryUpdate event.
this.$wrapper.trigger('summaryUpdated');
var isIE = /*@cc_on!@*/false || !!document.documentMode;
if (isIE) {
var $image = this.$image;
$('.image-data__crop-wrapper > summary').on('click', function () {
setTimeout(function () {$image.trigger('visible.iwc')}, 100);
});
}
};
/**
* Initializes the Cropper plugin.
*/
Drupal.ImageWidgetCropType.prototype.initializeCropper = function () {
// Calculate minimal height for cropper container (minimal width is 200).
var minDelta = (this.originalWidth / 200);
this.options.minContainerHeight = this.originalHeight / minDelta;
// Only autoCrop if 'Show default crop' is checked or if there is a crop already set.
this.options.autoCrop = this.showDefaultCrop || !!this.getValue('applied');
// Set aspect ratio.
this.options.aspectRatio = this.ratio;
this.$image
.on('ready.iwc.cropper', this.cropperReady.bind(this))
.on('cropend.iwc.cropper', this.cropEnd.bind(this))
.on('cropmove.iwc.cropper', this.cropMove.bind(this))
.cropper(this.options);
this.cropper = this.$image.data('cropper');
this.options = this.cropper.options;
// If "Show default crop" is checked apply default crop.
if (this.showDefaultCrop) {
// All data returned by cropper plugin multiple with delta in order to get
// proper crop sizes for original image.
this.setValue(this.$image.cropper('getData'), this.naturalDelta);
this.$wrapper.trigger('summaryUpdated');
}
};
/**
* Creates a poll that checks visibility of an item.
*
* @param {HTMLElement|jQuery} element
* The element to poll.
*
* Replace once vertical tabs have proper events ?
* When following issue are fixed @see https://www.drupal.org/node/2653570.
*/
Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) {
var $element = $(element);
// Immediately return if there's no element.
if (!$element[0]) {
return;
}
var isElementVisible = function (el) {
var rect = el.getBoundingClientRect();
var vWidth = window.innerWidth || document.documentElement.clientWidth;
var vHeight = window.innerHeight || document.documentElement.clientHeight;
// Immediately Return false if it's not in the viewport.
if (rect.right < 0 || rect.bottom < 0 || rect.left > vWidth || rect.top > vHeight) {
return false;
}
// Return true if any of its four corners are visible.
var efp = function (x, y) {
return document.elementFromPoint(x, y);
};
return (
el.contains(efp(rect.left, rect.top))
|| el.contains(efp(rect.right, rect.top))
|| el.contains(efp(rect.right, rect.bottom))
|| el.contains(efp(rect.left, rect.bottom))
);
};
var value = null;
var interval = setInterval(function () {
var visible = isElementVisible($element[0]);
if (value !== visible) {
$element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc');
}
}, 250);
this.intervals[interval] = $element;
};
/**
* Parses a ration value into a numeric one.
*
* @param {String} ratio
* A string representation of the ratio.
*
* @return {Number.<float>|NaN}
* The numeric representation of the ratio.
*/
Drupal.ImageWidgetCropType.prototype.parseRatio = function (ratio) {
if (ratio && /:/.test(ratio)) {
var parts = ratio.split(':');
var num1 = parseInt(parts[0], 10);
var num2 = parseInt(parts[1], 10);
return num1 / num2;
}
return parseFloat(ratio);
};
/**
* Reset cropping for an element.
*
* @param {Event} e
* The event object.
*/
Drupal.ImageWidgetCropType.prototype.reset = function (e) {
if (!this.cropper) {
return;
}
if (e instanceof Event || e instanceof $.Event) {
e.preventDefault();
e.stopPropagation();
}
this.options = $.extend({}, this.cropper.options, this.defaultOptions);
var delta = null;
// Retrieve all current values and zero (0) them out.
var values = this.getValues();
for (var name in values) {
if (values.hasOwnProperty(name)) {
values[name] = 0;
}
}
// If 'Show default crop' is not checked just re-initialize the cropper.
if (!this.showDefaultCrop) {
this.destroyCropper();
this.initializeCropper();
}
// Reset cropper to the original values.
else {
this.cropper.reset();
this.cropper.options = this.options;
// Set the delta.
delta = this.naturalDelta;
// Merge in the original cropper values.
values = $.extend(values, this.cropper.getData());
}
this.setValues(values, delta);
this.$wrapper.trigger('summaryUpdated');
};
/**
* The "resize" event handler proxied from the main instance.
*
* @see Drupal.ImageWidgetCrop.prototype.resize
*/
Drupal.ImageWidgetCropType.prototype.resize = function () {
// Immediately return if currently not visible.
if (!this.visible) {
return;
}
// Get previous data for cropper.
var canvasDataOld = this.$image.cropper('getCanvasData');
var cropBoxData = this.$image.cropper('getCropBoxData');
// Re-render cropper.
this.$image.cropper('render');
// Get new data for cropper and calculate resize ratio.
var canvasDataNew = this.$image.cropper('getCanvasData');
var ratio = 1;
if (canvasDataOld.width !== 0) {
ratio = canvasDataNew.width / canvasDataOld.width;
}
// Set new data for crop box.
$.each(cropBoxData, function (index, value) {
cropBoxData[index] = value * ratio;
});
this.$image.cropper('setCropBoxData', cropBoxData);
this.updateHardLimits();
this.updateSoftLimits();
this.$wrapper.trigger('summaryUpdated');
};
/**
* Sets a single crop value.
*
* @param {'applied'|'height'|'width'|'x'|'y'} name
* The name of the crop value to set.
* @param {Number} value
* The value to set.
* @param {Number} [delta]
* A delta to modify the value with.
*/
Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) {
if (!this.values.hasOwnProperty(name) || !this.values[name][0]) {
return;
}
value = value ? parseFloat(value) : 0;
if (delta && name !== 'applied') {
value = Math.floor(value * delta);
// Bind height and width to image size when below hard limit. Solves floating-point bug.
if (value < this.hardLimit[name]) {
value = name === 'width' ? this.originalWidth : name === 'height' ? this.originalHeight : null;
}
}
value = this.sanitizeSizes(name, value);
this.values[name][0].value = value;
this.values[name].trigger('change.iwc, input.iwc');
};
/**
* Validate and sanitize with or height sizes to avoid overflow.
*
* @param {'applied'|'height'|'width'|'x'|'y'} name
* The name of the crop value to set.
* @param {Number} value
* The value to set.
*/
Drupal.ImageWidgetCropType.prototype.sanitizeSizes = function (name, value) {
if (name === "width" || name === "height") {
return this.recalculateOverflowSizes(name, value);
}
return value;
};
/**
* Recalculate with or height sizes to avoid overflow for width or height.
*
* @param {'height'|'width'} name
* The name of the crop value to set.
* @param {Number} value
* The value to set.
*/
Drupal.ImageWidgetCropType.prototype.recalculateOverflowSizes = function (name, value) {
var originalValue = 'original' + name.capitalize();
if (value > this[originalValue]) {
value--;
return value;
}
return value;
};
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
/**
* Sets multiple crop values.
*
* @param {{applied: Number, height: Number, width: Number, x: Number, y: Number}} obj
* An object of key/value pairs of values to set.
* @param {Number} [delta]
* A delta to modify the value with.
*/
Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) {
for (var name in obj) {
if (!obj.hasOwnProperty(name)) {
continue;
}
this.setValue(name, obj[name], delta);
}
};
/**
* Converts horizontal and vertical dimensions to canvas dimensions.
*
* @param {Number} x - horizontal dimension in image space.
* @param {Number} y - vertical dimension in image space.
*/
Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) {
var imageData = this.cropper.getImageData();
return {
width: imageData.width * (x / this.originalWidth),
height: imageData.height * (y / this.originalHeight)
}
};
/**
* Converts horizontal and vertical dimensions to image dimensions.
*
* @param {Number} x - horizontal dimension in canvas space.
* @param {Number} y - vertical dimension in canvas space.
*/
Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) {
var imageData = this.cropper.getImageData();
return {
width: x * (this.originalWidth / imageData.width),
height: y * (this.originalHeight / imageData.height)
}
};
/**
* Update hard limits.
*/
Drupal.ImageWidgetCropType.prototype.updateHardLimits = function () {
// Immediately return if there is no cropper plugin instance or hard limits.
if (!this.cropper || !this.hardLimit.width || !this.hardLimit.height) {
return;
}
var options = this.cropper.options;
// Limits works in canvas so we need to convert dimensions.
var converted = this.toCanvasDimensions(this.hardLimit.width, this.hardLimit.height);
options.minCropBoxWidth = converted.width;
options.minCropBoxHeight = converted.height;
// After updating the options we need to limit crop box.
this.cropper.limitCropBox(true, false);
};
/**
* Update soft limits.
*/
Drupal.ImageWidgetCropType.prototype.updateSoftLimits = function () {
// Immediately return if there is no cropper plugin instance or soft limits.
if (!this.cropper || !this.softLimit.width || !this.softLimit.height) {
return;
}
// We do comparison in image dimensions so lets convert first.
var cropBoxData = this.cropper.getCropBoxData();
var converted = this.toImageDimensions(cropBoxData.width, cropBoxData.height);
var dimensions = ['width', 'height'];
for (var i = 0, l = dimensions.length; i < l; i++) {
var dimension = dimensions[i];
if (converted[dimension] < this.softLimit[dimension]) {
if (!this.softLimit.reached[dimension]) {
this.softLimit.reached[dimension] = true;
}
}
else if (this.softLimit.reached[dimension]) {
this.softLimit.reached[dimension] = false;
}
this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]);
}
this.$wrapper.trigger('summaryUpdated');
};
/**
* Updates the summary of the wrapper.
*/
Drupal.ImageWidgetCropType.prototype.updateSummary = function () {
var summary = [];
if (this.getValue('applied')) {
summary.push(Drupal.t('Cropping applied.'));
}
if (this.softLimit.reached.height || this.softLimit.reached.width) {
summary.push(Drupal.t('Soft limit reached.'));
}
return summary.join('<br>');
};
}(jQuery, Drupal));
