X-Git-Url: http://aleph1.co.uk/gitweb/?a=blobdiff_plain;f=web%2Fmodules%2Fcontrib%2Fimage_widget_crop%2Fjs%2FImageWidgetCropType.js;fp=web%2Fmodules%2Fcontrib%2Fimage_widget_crop%2Fjs%2FImageWidgetCropType.js;h=f9c376b6e2c853324738128598784381c2ea2354;hb=eba34333e3c89f208d2f72fa91351ad019a71583;hp=0000000000000000000000000000000000000000;hpb=a2bd1bf0c2c1f1a17d188f4dc0726a45494cefae;p=yaffs-website diff --git a/web/modules/contrib/image_widget_crop/js/ImageWidgetCropType.js b/web/modules/contrib/image_widget_crop/js/ImageWidgetCropType.js new file mode 100644 index 000000000..f9c376b6e --- /dev/null +++ b/web/modules/contrib/image_widget_crop/js/ImageWidgetCropType.js @@ -0,0 +1,765 @@ +/** + * @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.} + */ + 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; + + /** + * 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.} + */ + 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]', // @todo is this even used anymore? + 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 "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.updateSoftLimits(); + }; + + /** + * 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.round(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); + + // Extend this instance with data from the wrapper. + var data = this.$wrapper.data(); + for (var i in data) { + if (data.hasOwnProperty(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'); + }; + + /** + * 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. + this.options.autoCrop = this.showDefaultCrop; + + // Set aspect ratio. + this.options.aspectRatio = this.ratio; + + // Initialize data. + var values = this.getValues(this.naturalDelta); + this.options.data = this.options.data || {}; + if (values.applied) { + // Remove the "applied" value as it has no meaning in Cropper. + delete values.applied; + + // Merge in the values. + this.options.data = $.extend(true, this.options.data, values); + + // Enforce autoCrop if there's currently a crop applied. + this.options.autoCrop = true; + } + + this.options.data.rotate = 0; + this.options.data.scaleX = 1; + this.options.data.scaleY = 1; + + this.$image + .on('built.iwc.cropper', this.built.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. + * + * @todo Perhaps replace once vertical tabs have proper events? + * + * @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.|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 ? parseInt(value, 10) : 0; + if (delta && name !== 'applied') { + value = Math.round(value * delta); + } + this.values[name][0].value = value; + this.values[name].trigger('change.iwc, input.iwc'); + }; + + /** + * 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('
'); + }; + +}(jQuery, Drupal));