Yaffs site version 1.1
[yaffs-website] / web / modules / contrib / image_widget_crop / js / ImageWidgetCropType.js
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 (file)
index 0000000..f9c376b
--- /dev/null
@@ -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.<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;
+
+    /**
+     * 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]', // @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.<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 ? 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('<br>');
+  };
+
+}(jQuery, Drupal));