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]', // @todo is this even used anymore?
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;
417 .on('click.iwc', this.reset.bind(this))
420 // Star polling visibility of the image that should be able to be cropped.
421 this.pollVisibility(this.$image);
423 // Bind the drupalSetSummary callback.
424 this.$wrapper.drupalSetSummary(this.updateSummary.bind(this));
426 // Trigger the initial summaryUpdate event.
427 this.$wrapper.trigger('summaryUpdated');
431 * Initializes the Cropper plugin.
433 Drupal.ImageWidgetCropType.prototype.initializeCropper = function () {
434 // Calculate minimal height for cropper container (minimal width is 200).
435 var minDelta = (this.originalWidth / 200);
436 this.options.minContainerHeight = this.originalHeight / minDelta;
438 // Only autoCrop if 'Show default crop' is checked.
439 this.options.autoCrop = this.showDefaultCrop;
442 this.options.aspectRatio = this.ratio;
445 var values = this.getValues(this.naturalDelta);
446 this.options.data = this.options.data || {};
447 if (values.applied) {
448 // Remove the "applied" value as it has no meaning in Cropper.
449 delete values.applied;
451 // Merge in the values.
452 this.options.data = $.extend(true, this.options.data, values);
454 // Enforce autoCrop if there's currently a crop applied.
455 this.options.autoCrop = true;
458 this.options.data.rotate = 0;
459 this.options.data.scaleX = 1;
460 this.options.data.scaleY = 1;
463 .on('built.iwc.cropper', this.built.bind(this))
464 .on('cropend.iwc.cropper', this.cropEnd.bind(this))
465 .on('cropmove.iwc.cropper', this.cropMove.bind(this))
466 .cropper(this.options)
469 this.cropper = this.$image.data('cropper');
470 this.options = this.cropper.options;
472 // If "Show default crop" is checked apply default crop.
473 if (this.showDefaultCrop) {
474 // All data returned by cropper plugin multiple with delta in order to get
475 // proper crop sizes for original image.
476 this.setValue(this.$image.cropper('getData'), this.naturalDelta);
477 this.$wrapper.trigger('summaryUpdated');
482 * Creates a poll that checks visibility of an item.
484 * @param {HTMLElement|jQuery} element
485 * The element to poll.
487 * @todo Perhaps replace once vertical tabs have proper events?
489 * @see https://www.drupal.org/node/2653570
491 Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) {
492 var $element = $(element);
494 // Immediately return if there's no element.
499 var isElementVisible = function (el) {
500 var rect = el.getBoundingClientRect();
501 var vWidth = window.innerWidth || document.documentElement.clientWidth;
502 var vHeight = window.innerHeight || document.documentElement.clientHeight;
504 // Immediately Return false if it's not in the viewport.
505 if (rect.right < 0 || rect.bottom < 0 || rect.left > vWidth || rect.top > vHeight) {
509 // Return true if any of its four corners are visible.
510 var efp = function (x, y) {
511 return document.elementFromPoint(x, y);
514 el.contains(efp(rect.left, rect.top))
515 || el.contains(efp(rect.right, rect.top))
516 || el.contains(efp(rect.right, rect.bottom))
517 || el.contains(efp(rect.left, rect.bottom))
522 var interval = setInterval(function () {
523 var visible = isElementVisible($element[0]);
524 if (value !== visible) {
525 $element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc');
528 this.intervals[interval] = $element;
532 * Parses a ration value into a numeric one.
534 * @param {String} ratio
535 * A string representation of the ratio.
537 * @return {Number.<float>|NaN}
538 * The numeric representation of the ratio.
540 Drupal.ImageWidgetCropType.prototype.parseRatio = function (ratio) {
541 if (ratio && /:/.test(ratio)) {
542 var parts = ratio.split(':');
543 var num1 = parseInt(parts[0], 10);
544 var num2 = parseInt(parts[1], 10);
547 return parseFloat(ratio);
551 * Reset cropping for an element.
556 Drupal.ImageWidgetCropType.prototype.reset = function (e) {
561 if (e instanceof Event || e instanceof $.Event) {
566 this.options = $.extend({}, this.cropper.options, this.defaultOptions);
570 // Retrieve all current values and zero (0) them out.
571 var values = this.getValues();
572 for (var name in values) {
573 if (values.hasOwnProperty(name)) {
578 // If 'Show default crop' is not checked just re-initialize the cropper.
579 if (!this.showDefaultCrop) {
580 this.destroyCropper();
581 this.initializeCropper();
583 // Reset cropper to the original values.
585 this.cropper.reset();
586 this.cropper.options = this.options;
589 delta = this.naturalDelta;
591 // Merge in the original cropper values.
592 values = $.extend(values, this.cropper.getData());
595 this.setValues(values, delta);
596 this.$wrapper.trigger('summaryUpdated');
600 * The "resize" event handler proxied from the main instance.
602 * @see Drupal.ImageWidgetCrop.prototype.resize
604 Drupal.ImageWidgetCropType.prototype.resize = function () {
605 // Immediately return if currently not visible.
610 // Get previous data for cropper.
611 var canvasDataOld = this.$image.cropper('getCanvasData');
612 var cropBoxData = this.$image.cropper('getCropBoxData');
614 // Re-render cropper.
615 this.$image.cropper('render');
617 // Get new data for cropper and calculate resize ratio.
618 var canvasDataNew = this.$image.cropper('getCanvasData');
620 if (canvasDataOld.width !== 0) {
621 ratio = canvasDataNew.width / canvasDataOld.width;
624 // Set new data for crop box.
625 $.each(cropBoxData, function (index, value) {
626 cropBoxData[index] = value * ratio;
628 this.$image.cropper('setCropBoxData', cropBoxData);
630 this.updateHardLimits();
631 this.updateSoftLimits();
632 this.$wrapper.trigger('summaryUpdated');
636 * Sets a single crop value.
638 * @param {'applied'|'height'|'width'|'x'|'y'} name
639 * The name of the crop value to set.
640 * @param {Number} value
642 * @param {Number} [delta]
643 * A delta to modify the value with.
645 Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) {
646 if (!this.values.hasOwnProperty(name) || !this.values[name][0]) {
649 value = value ? parseInt(value, 10) : 0;
650 if (delta && name !== 'applied') {
651 value = Math.round(value * delta);
653 this.values[name][0].value = value;
654 this.values[name].trigger('change.iwc, input.iwc');
658 * Sets multiple crop values.
660 * @param {{applied: Number, height: Number, width: Number, x: Number, y: Number}} obj
661 * An object of key/value pairs of values to set.
662 * @param {Number} [delta]
663 * A delta to modify the value with.
665 Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) {
666 for (var name in obj) {
667 if (!obj.hasOwnProperty(name)) {
670 this.setValue(name, obj[name], delta);
675 * Converts horizontal and vertical dimensions to canvas dimensions.
677 * @param {Number} x - horizontal dimension in image space.
678 * @param {Number} y - vertical dimension in image space.
680 Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) {
681 var imageData = this.cropper.getImageData();
683 width: imageData.width * (x / this.originalWidth),
684 height: imageData.height * (y / this.originalHeight)
689 * Converts horizontal and vertical dimensions to image dimensions.
691 * @param {Number} x - horizontal dimension in canvas space.
692 * @param {Number} y - vertical dimension in canvas space.
694 Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) {
695 var imageData = this.cropper.getImageData();
697 width: x * (this.originalWidth / imageData.width),
698 height: y * (this.originalHeight / imageData.height)
703 * Update hard limits.
705 Drupal.ImageWidgetCropType.prototype.updateHardLimits = function () {
706 // Immediately return if there is no cropper plugin instance or hard limits.
707 if (!this.cropper || !this.hardLimit.width || !this.hardLimit.height) {
711 var options = this.cropper.options;
713 // Limits works in canvas so we need to convert dimensions.
714 var converted = this.toCanvasDimensions(this.hardLimit.width, this.hardLimit.height);
715 options.minCropBoxWidth = converted.width;
716 options.minCropBoxHeight = converted.height;
718 // After updating the options we need to limit crop box.
719 this.cropper.limitCropBox(true, false);
723 * Update soft limits.
725 Drupal.ImageWidgetCropType.prototype.updateSoftLimits = function () {
726 // Immediately return if there is no cropper plugin instance or soft limits.
727 if (!this.cropper || !this.softLimit.width || !this.softLimit.height) {
731 // We do comparison in image dimensions so lets convert first.
732 var cropBoxData = this.cropper.getCropBoxData();
733 var converted = this.toImageDimensions(cropBoxData.width, cropBoxData.height);
735 var dimensions = ['width', 'height'];
736 for (var i = 0, l = dimensions.length; i < l; i++) {
737 var dimension = dimensions[i];
738 if (converted[dimension] < this.softLimit[dimension]) {
739 if (!this.softLimit.reached[dimension]) {
740 this.softLimit.reached[dimension] = true;
743 else if (this.softLimit.reached[dimension]) {
744 this.softLimit.reached[dimension] = false;
746 this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]);
748 this.$wrapper.trigger('summaryUpdated');
752 * Updates the summary of the wrapper.
754 Drupal.ImageWidgetCropType.prototype.updateSummary = function () {
756 if (this.getValue('applied')) {
757 summary.push(Drupal.t('Cropping applied.'));
759 if (this.softLimit.reached.height || this.softLimit.reached.width) {
760 summary.push(Drupal.t('Soft limit reached.'));
762 return summary.join('<br>');