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 * Flag indicating whether to show the default crop.
163 this.isRequired = false;
166 * The soft limit of the crop.
168 * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
180 * The numeric representation of a ratio.
187 * The value elements.
189 * @type {Object.<String, jQuery>}
192 applied: this.$wrapper.find(this.selectors.values.applied),
193 height: this.$wrapper.find(this.selectors.values.height),
194 width: this.$wrapper.find(this.selectors.values.width),
195 x: this.$wrapper.find(this.selectors.values.x),
196 y: this.$wrapper.find(this.selectors.values.y)
200 * Flag indicating whether the instance is currently visible.
204 this.visible = false;
206 // Initialize the instance.
211 * The prefix used for all Image Widget Crop data attributes.
215 Drupal.ImageWidgetCropType.prototype.dataPrefix = /^drupalIwc/;
218 * Default options to pass to the Cropper plugin.
222 Drupal.ImageWidgetCropType.prototype.defaultOptions = {
231 * The selectors used to identify elements for this module.
235 Drupal.ImageWidgetCropType.prototype.selectors = {
236 image: '[data-drupal-iwc=image]',
237 reset: '[data-drupal-iwc=reset]',
238 table: '[data-drupal-iwc=table]',
240 applied: '[data-drupal-iwc-value=applied]',
241 height: '[data-drupal-iwc-value=height]',
242 width: '[data-drupal-iwc-value=width]',
243 x: '[data-drupal-iwc-value=x]',
244 y: '[data-drupal-iwc-value=y]'
249 * The "built" event handler for the Cropper plugin.
251 Drupal.ImageWidgetCropType.prototype.built = function () {
252 this.$cropperWrapper = this.$wrapper.find('.cropper-container');
253 this.updateHardLimits();
254 this.updateSoftLimits();
258 * The "cropend" event handler for the Cropper plugin.
260 Drupal.ImageWidgetCropType.prototype.cropEnd = function () {
261 // Immediately return if there is no cropper instance (for whatever reason).
266 // Retrieve the cropper data.
267 var data = this.cropper.getData();
269 // Ensure the applied state is enabled.
272 // Data returned by Cropper plugin should be multiplied with delta in order
273 // to get the proper crop sizes for the original image.
274 this.setValues(data, this.naturalDelta);
276 // Trigger summary updates.
277 this.$wrapper.trigger('summaryUpdated');
281 * The "cropmove" event handler for the Cropper plugin.
283 Drupal.ImageWidgetCropType.prototype.cropMove = function () {
288 * Destroys this instance.
290 Drupal.ImageWidgetCropType.prototype.destroy = function () {
291 this.destroyCropper();
293 this.$image.off('.iwc');
294 this.$reset.off('.iwc');
296 // Clear any intervals that were set.
297 for (var interval in this.intervals) {
298 if (this.intervals.hasOwnProperty(interval)) {
299 clearInterval(interval);
300 delete this.intervals[interval];
306 * Destroys the Cropper plugin instance.
308 Drupal.ImageWidgetCropType.prototype.destroyCropper = function () {
309 this.$image.off('.iwc.cropper');
311 this.cropper.destroy();
317 * Disables this instance.
319 Drupal.ImageWidgetCropType.prototype.disable = function () {
321 this.cropper.disable();
323 this.$table.removeClass('responsive-enabled--opened');
327 * Enables this instance.
329 Drupal.ImageWidgetCropType.prototype.enable = function () {
331 this.cropper.enable();
333 this.$table.addClass('responsive-enabled--opened');
337 * Retrieves a crop value.
339 * @param {'applied'|'height'|'width'|'x'|'y'} name
340 * The name of the crop value to retrieve.
341 * @param {Number} [delta]
342 * The delta amount to divide value by, if any.
347 Drupal.ImageWidgetCropType.prototype.getValue = function (name, delta) {
349 if (this.values[name] && this.values[name][0]) {
350 value = parseInt(this.values[name][0].value, 10) || 0;
352 return name !== 'applied' && value && delta ? Math.round(value / delta) : value;
356 * Retrieves all crop values.
358 * @param {Number} [delta]
359 * The delta amount to divide value by, if any.
361 * @return {{applied: Number, height: Number, width: Number, x: Number, y: Number}}
362 * The crop value key/value pairs.
364 Drupal.ImageWidgetCropType.prototype.getValues = function (delta) {
366 for (var name in this.values) {
367 if (this.values.hasOwnProperty(name)) {
368 values[name] = this.getValue(name, delta);
375 * Initializes the instance.
377 Drupal.ImageWidgetCropType.prototype.init = function () {
378 // Immediately return if already initialized.
379 if (this.initialized) {
383 // Set the default options.
384 this.options = $.extend({}, this.defaultOptions);
385 this.isRequired = this.$wrapper.data('drupalIwcRequired');
387 // Extend this instance with data from the wrapper.
388 var data = this.$wrapper.data();
389 for (var i in data) {
390 if (data.hasOwnProperty(i) && this.dataPrefix.test(i)) {
391 // Remove Drupal + module prefix and lowercase the first letter.
392 var prop = i.replace(this.dataPrefix, '');
393 prop = prop.charAt(0).toLowerCase() + prop.slice(1);
395 // Check if data attribute exists on this object.
396 if (prop && this.hasOwnProperty(prop)) {
399 // Parse the ratio value.
400 if (prop === 'ratio') {
401 value = this.parseRatio(value);
403 this[prop] = typeof value === 'object' ? $.extend(true, {}, this[prop], value) : value;
408 // Bind necessary events.
410 .on('visible.iwc', function () {
412 this.naturalHeight = parseInt(this.$image.prop('naturalHeight'), 10);
413 this.naturalWidth = parseInt(this.$image.prop('naturalWidth'), 10);
414 // Calculate delta between original and thumbnail images.
415 this.naturalDelta = this.originalHeight && this.naturalHeight ? this.originalHeight / this.naturalHeight : null;
417 // Only initialize the cropper plugin once.
418 .one('visible.iwc', this.initializeCropper.bind(this))
419 .on('hidden.iwc', function () {
420 this.visible = false;
424 .on('click.iwc', this.reset.bind(this));
426 // Star polling visibility of the image that should be able to be cropped.
427 this.pollVisibility(this.$image);
429 // Bind the drupalSetSummary callback.
430 this.$wrapper.drupalSetSummary(this.updateSummary.bind(this));
432 // Trigger the initial summaryUpdate event.
433 this.$wrapper.trigger('summaryUpdated');
434 var isIE = /*@cc_on!@*/false || !!document.documentMode;
436 var $image = this.$image;
437 $('.image-data__crop-wrapper > summary').on('click', function () {
438 setTimeout(function () {$image.trigger('visible.iwc')}, 100);
444 * Initializes the Cropper plugin.
446 Drupal.ImageWidgetCropType.prototype.initializeCropper = function () {
447 // Calculate minimal height for cropper container (minimal width is 200).
448 var minDelta = (this.originalWidth / 200);
449 this.options.minContainerHeight = this.originalHeight / minDelta;
451 // Only autoCrop if 'Show default crop' is checked.
452 this.options.autoCrop = this.showDefaultCrop;
455 this.options.aspectRatio = this.ratio;
458 var values = this.getValues(this.naturalDelta);
459 this.options.data = this.options.data || {};
460 if (values.applied) {
461 // Remove the "applied" value as it has no meaning in Cropper.
462 delete values.applied;
464 // Merge in the values.
465 this.options.data = $.extend(true, this.options.data, values);
467 // Enforce autoCrop if there's currently a crop applied.
468 this.options.autoCrop = true;
471 this.options.data.scaleX = 1;
472 this.options.data.scaleY = 1;
475 .on('built.iwc.cropper', this.built.bind(this))
476 .on('cropend.iwc.cropper', this.cropEnd.bind(this))
477 .on('cropmove.iwc.cropper', this.cropMove.bind(this))
478 .cropper(this.options);
480 this.cropper = this.$image.data('cropper');
481 this.options = this.cropper.options;
483 // If "Show default crop" is checked apply default crop.
484 if (this.showDefaultCrop) {
485 // All data returned by cropper plugin multiple with delta in order to get
486 // proper crop sizes for original image.
487 this.setValue(this.$image.cropper('getData'), this.naturalDelta);
488 this.$wrapper.trigger('summaryUpdated');
493 * Creates a poll that checks visibility of an item.
495 * @param {HTMLElement|jQuery} element
496 * The element to poll.
498 * Replace once vertical tabs have proper events ?
499 * When following issue are fixed @see https://www.drupal.org/node/2653570.
501 Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) {
502 var $element = $(element);
504 // Immediately return if there's no element.
509 var isElementVisible = function (el) {
510 var rect = el.getBoundingClientRect();
511 var vWidth = window.innerWidth || document.documentElement.clientWidth;
512 var vHeight = window.innerHeight || document.documentElement.clientHeight;
514 // Immediately Return false if it's not in the viewport.
515 if (rect.right < 0 || rect.bottom < 0 || rect.left > vWidth || rect.top > vHeight) {
519 // Return true if any of its four corners are visible.
520 var efp = function (x, y) {
521 return document.elementFromPoint(x, y);
524 el.contains(efp(rect.left, rect.top))
525 || el.contains(efp(rect.right, rect.top))
526 || el.contains(efp(rect.right, rect.bottom))
527 || el.contains(efp(rect.left, rect.bottom))
532 var interval = setInterval(function () {
533 var visible = isElementVisible($element[0]);
534 if (value !== visible) {
535 $element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc');
538 this.intervals[interval] = $element;
542 * Parses a ration value into a numeric one.
544 * @param {String} ratio
545 * A string representation of the ratio.
547 * @return {Number.<float>|NaN}
548 * The numeric representation of the ratio.
550 Drupal.ImageWidgetCropType.prototype.parseRatio = function (ratio) {
551 if (ratio && /:/.test(ratio)) {
552 var parts = ratio.split(':');
553 var num1 = parseInt(parts[0], 10);
554 var num2 = parseInt(parts[1], 10);
557 return parseFloat(ratio);
561 * Reset cropping for an element.
566 Drupal.ImageWidgetCropType.prototype.reset = function (e) {
571 if (e instanceof Event || e instanceof $.Event) {
576 this.options = $.extend({}, this.cropper.options, this.defaultOptions);
580 // Retrieve all current values and zero (0) them out.
581 var values = this.getValues();
582 for (var name in values) {
583 if (values.hasOwnProperty(name)) {
588 // If 'Show default crop' is not checked just re-initialize the cropper.
589 if (!this.showDefaultCrop) {
590 this.destroyCropper();
591 this.initializeCropper();
593 // Reset cropper to the original values.
595 this.cropper.reset();
596 this.cropper.options = this.options;
599 delta = this.naturalDelta;
601 // Merge in the original cropper values.
602 values = $.extend(values, this.cropper.getData());
605 this.setValues(values, delta);
606 this.$wrapper.trigger('summaryUpdated');
610 * The "resize" event handler proxied from the main instance.
612 * @see Drupal.ImageWidgetCrop.prototype.resize
614 Drupal.ImageWidgetCropType.prototype.resize = function () {
615 // Immediately return if currently not visible.
620 // Get previous data for cropper.
621 var canvasDataOld = this.$image.cropper('getCanvasData');
622 var cropBoxData = this.$image.cropper('getCropBoxData');
624 // Re-render cropper.
625 this.$image.cropper('render');
627 // Get new data for cropper and calculate resize ratio.
628 var canvasDataNew = this.$image.cropper('getCanvasData');
630 if (canvasDataOld.width !== 0) {
631 ratio = canvasDataNew.width / canvasDataOld.width;
634 // Set new data for crop box.
635 $.each(cropBoxData, function (index, value) {
636 cropBoxData[index] = value * ratio;
638 this.$image.cropper('setCropBoxData', cropBoxData);
640 this.updateHardLimits();
641 this.updateSoftLimits();
642 this.$wrapper.trigger('summaryUpdated');
646 * Sets a single crop value.
648 * @param {'applied'|'height'|'width'|'x'|'y'} name
649 * The name of the crop value to set.
650 * @param {Number} value
652 * @param {Number} [delta]
653 * A delta to modify the value with.
655 Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) {
656 if (!this.values.hasOwnProperty(name) || !this.values[name][0]) {
659 value = value ? parseFloat(value) : 0;
660 if (delta && name !== 'applied') {
661 value = Math.round(value * delta);
663 this.values[name][0].value = value;
664 this.values[name].trigger('change.iwc, input.iwc');
668 * Sets multiple crop values.
670 * @param {{applied: Number, height: Number, width: Number, x: Number, y: Number}} obj
671 * An object of key/value pairs of values to set.
672 * @param {Number} [delta]
673 * A delta to modify the value with.
675 Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) {
676 for (var name in obj) {
677 if (!obj.hasOwnProperty(name)) {
680 this.setValue(name, obj[name], delta);
685 * Converts horizontal and vertical dimensions to canvas dimensions.
687 * @param {Number} x - horizontal dimension in image space.
688 * @param {Number} y - vertical dimension in image space.
690 Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) {
691 var imageData = this.cropper.getImageData();
693 width: imageData.width * (x / this.originalWidth),
694 height: imageData.height * (y / this.originalHeight)
699 * Converts horizontal and vertical dimensions to image dimensions.
701 * @param {Number} x - horizontal dimension in canvas space.
702 * @param {Number} y - vertical dimension in canvas space.
704 Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) {
705 var imageData = this.cropper.getImageData();
707 width: x * (this.originalWidth / imageData.width),
708 height: y * (this.originalHeight / imageData.height)
713 * Update hard limits.
715 Drupal.ImageWidgetCropType.prototype.updateHardLimits = function () {
716 // Immediately return if there is no cropper plugin instance or hard limits.
717 if (!this.cropper || !this.hardLimit.width || !this.hardLimit.height) {
721 var options = this.cropper.options;
723 // Limits works in canvas so we need to convert dimensions.
724 var converted = this.toCanvasDimensions(this.hardLimit.width, this.hardLimit.height);
725 options.minCropBoxWidth = converted.width;
726 options.minCropBoxHeight = converted.height;
728 // After updating the options we need to limit crop box.
729 this.cropper.limitCropBox(true, false);
733 * Update soft limits.
735 Drupal.ImageWidgetCropType.prototype.updateSoftLimits = function () {
736 // Immediately return if there is no cropper plugin instance or soft limits.
737 if (!this.cropper || !this.softLimit.width || !this.softLimit.height) {
741 // We do comparison in image dimensions so lets convert first.
742 var cropBoxData = this.cropper.getCropBoxData();
743 var converted = this.toImageDimensions(cropBoxData.width, cropBoxData.height);
745 var dimensions = ['width', 'height'];
746 for (var i = 0, l = dimensions.length; i < l; i++) {
747 var dimension = dimensions[i];
748 if (converted[dimension] < this.softLimit[dimension]) {
749 if (!this.softLimit.reached[dimension]) {
750 this.softLimit.reached[dimension] = true;
753 else if (this.softLimit.reached[dimension]) {
754 this.softLimit.reached[dimension] = false;
756 this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]);
758 this.$wrapper.trigger('summaryUpdated');
762 * Updates the summary of the wrapper.
764 Drupal.ImageWidgetCropType.prototype.updateSummary = function () {
766 if (this.getValue('applied')) {
767 summary.push(Drupal.t('Cropping applied.'));
769 if (this.softLimit.reached.height || this.softLimit.reached.width) {
770 summary.push(Drupal.t('Soft limit reached.'));
772 return summary.join('<br>');
776 * Override Theme function for a vertical tabs.
778 * @param {object} settings
779 * An object with the following keys:
780 * @param {string} settings.title
781 * The name of the tab.
784 * This function has to return an object with at least these keys:
785 * - item: The root tab jQuery element
786 * - link: The anchor tag that acts as the clickable area of the tab
788 * - summary: The jQuery element that contains the tab summary
790 Drupal.theme.verticalTab = function (settings) {
792 this.isRequired = settings.details.data('drupalIwcRequired');
793 tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>').append(tab.link = $('<a href="#"></a>').append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title)).append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>')));
795 // If those Crop type is required add attributes.
796 if (this.isRequired) {
797 tab.title.addClass('js-form-required form-required');