13cbaf11cfd80f69463fc42179ef7a4a96467cd8
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Public / JavaScript / ImageManipulation.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13 define(["require", "exports", "jquery", "TYPO3/CMS/Backend/Modal", "TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min", "jquery-ui/draggable", "jquery-ui/resizable"], function (require, exports, $, Modal, ImagesLoaded) {
14 "use strict";
15 /**
16 * Module: TYPO3/CMS/Backend/ImageManipulation
17 * Contains all logic for the image crop GUI including setting focusAreas
18 * @exports TYPO3/CMS/Backend/ImageManipulation
19 */
20 var ImageManipulation = (function () {
21 function ImageManipulation() {
22 var _this = this;
23 this.cropImageContainerSelector = '#t3js-crop-image-container';
24 this.cropImageSelector = '#t3js-crop-image';
25 this.coverAreaSelector = '.t3js-cropper-cover-area';
26 this.cropInfoSelector = '.t3js-cropper-info-crop';
27 this.focusAreaSelector = '#t3js-cropper-focus-area';
28 this.defaultFocusArea = {
29 height: 1 / 3,
30 width: 1 / 3,
31 x: 0,
32 y: 0,
33 };
34 this.defaultOpts = {
35 autoCrop: true,
36 autoCropArea: '0.7',
37 dragMode: 'crop',
38 guides: true,
39 responsive: true,
40 viewMode: 1,
41 zoomable: false,
42 };
43 this.resizeTimeout = 450;
44 /**
45 * @method cropBuiltHandler
46 * @desc Internal cropper handler. Called when the cropper has been instantiated
47 * @private
48 */
49 this.cropBuiltHandler = function () {
50 var imageData = _this.cropper.cropper('getImageData');
51 var image = _this.currentModal.find(_this.cropImageSelector);
52 _this.imageOriginalSizeFactor = image.data('originalWidth') / imageData.naturalWidth;
53 // Iterate over the crop variants and set up their respective preview
54 _this.cropVariantTriggers.each(function (index, elem) {
55 var cropVariantId = $(elem).attr('data-crop-variant-id');
56 var cropArea = _this.convertRelativeToAbsoluteCropArea(_this.data[cropVariantId].cropArea, imageData);
57 var variant = $.extend(true, {}, _this.data[cropVariantId], { cropArea: cropArea });
58 _this.updatePreviewThumbnail(variant, $(elem));
59 });
60 _this.currentCropVariant.cropArea = _this.convertRelativeToAbsoluteCropArea(_this.currentCropVariant.cropArea, imageData);
61 // Can't use .t3js-* as selector because it is an extraneous selector
62 _this.cropBox = _this.currentModal.find('.cropper-crop-box');
63 _this.setCropArea(_this.currentCropVariant.cropArea);
64 // Check if new cropVariant has coverAreas
65 if (_this.currentCropVariant.coverAreas) {
66 // Init or reinit focusArea
67 _this.initCoverAreas(_this.cropBox, _this.currentCropVariant.coverAreas);
68 }
69 // Check if new cropVariant has focusArea
70 if (_this.currentCropVariant.focusArea) {
71 // Init or reinit focusArea
72 if (ImageManipulation.isEmptyArea(_this.currentCropVariant.focusArea)) {
73 // If an empty focusArea is set initialise it with the default
74 _this.currentCropVariant.focusArea = $.extend(true, {}, _this.defaultFocusArea);
75 }
76 _this.initFocusArea(_this.cropBox);
77 _this.scaleAndMoveFocusArea(_this.currentCropVariant.focusArea);
78 }
79 if (_this.currentCropVariant.selectedRatio) {
80 _this.setAspectRatio(_this.currentCropVariant.allowedAspectRatios[_this.currentCropVariant.selectedRatio]);
81 // Set data explicitly or setAspectRatio up-scales the crop
82 _this.setCropArea(_this.currentCropVariant.cropArea);
83 _this.currentModal.find("[data-option='" + _this.currentCropVariant.selectedRatio + "']").addClass('active');
84 }
85 _this.cropperCanvas.addClass('is-visible');
86 };
87 /**
88 * @method cropMoveHandler
89 * @desc Internal cropper handler. Called when the cropping area is moving
90 * @private
91 */
92 this.cropMoveHandler = function (e) {
93 _this.currentCropVariant.cropArea = $.extend(true, _this.currentCropVariant.cropArea, {
94 height: Math.floor(e.height),
95 width: Math.floor(e.width),
96 x: Math.floor(e.x),
97 y: Math.floor(e.y),
98 });
99 _this.updatePreviewThumbnail(_this.currentCropVariant, _this.activeCropVariantTrigger);
100 _this.updateCropVariantData(_this.currentCropVariant);
101 var naturalWidth = Math.round(_this.currentCropVariant.cropArea.width * _this.imageOriginalSizeFactor);
102 var naturalHeight = Math.round(_this.currentCropVariant.cropArea.height * _this.imageOriginalSizeFactor);
103 _this.cropInfo.text(naturalWidth + "\u00D7" + naturalHeight + " px");
104 };
105 /**
106 * @method cropStartHandler
107 * @desc Internal cropper handler. Called when the cropping starts moving
108 * @private
109 */
110 this.cropStartHandler = function () {
111 if (_this.currentCropVariant.focusArea) {
112 _this.focusArea.draggable('option', 'disabled', true);
113 _this.focusArea.resizable('option', 'disabled', true);
114 }
115 };
116 /**
117 * @method cropEndHandler
118 * @desc Internal cropper handler. Called when the cropping ends moving
119 * @private
120 */
121 this.cropEndHandler = function () {
122 if (_this.currentCropVariant.focusArea) {
123 _this.focusArea.draggable('option', 'disabled', false);
124 _this.focusArea.resizable('option', 'disabled', false);
125 }
126 };
127 // Silence is golden
128 $(window).resize(function () {
129 if (_this.cropper) {
130 _this.cropper.cropper('destroy');
131 }
132 });
133 this.resizeEnd(function () {
134 if (_this.cropper) {
135 _this.init();
136 }
137 });
138 }
139 /**
140 * @method isCropAreaEmpty
141 * @desc Checks if an area is set or pristine
142 * @param {Area} area - The area to check
143 * @return {boolean}
144 * @static
145 */
146 ImageManipulation.isEmptyArea = function (area) {
147 return $.isEmptyObject(area);
148 };
149 /**
150 * @method wait
151 * @desc window.setTimeout shim
152 * @param {Function} fn - The function to execute
153 * @param {number} ms - The time in [ms] to wait until execution
154 * @return {boolean}
155 * @public
156 * @static
157 */
158 ImageManipulation.wait = function (fn, ms) {
159 window.setTimeout(fn, ms);
160 };
161 /**
162 * @method toCssPercent
163 * @desc Takes a number, and converts it to CSS percentage length
164 * @param {number} num - The number to convert
165 * @return {string}
166 * @public
167 * @static
168 */
169 ImageManipulation.toCssPercent = function (num) {
170 return num * 100 + "%";
171 };
172 /**
173 * @method serializeCropVariants
174 * @desc Serializes crop variants for persistence or preview
175 * @param {Object} cropVariants
176 * @returns string
177 */
178 ImageManipulation.serializeCropVariants = function (cropVariants) {
179 var omitUnused = function (key, value) {
180 return (key === 'id'
181 || key === 'title'
182 || key === 'allowedAspectRatios'
183 || key === 'coverAreas') ? undefined : value;
184 };
185 return JSON.stringify(cropVariants, omitUnused);
186 };
187 /**
188 * @method initializeTrigger
189 * @desc Assign a handler to .t3js-image-manipulation-trigger.
190 * Show the modal and kick-off image manipulation
191 * @public
192 */
193 ImageManipulation.prototype.initializeTrigger = function () {
194 var _this = this;
195 var triggerHandler = function (e) {
196 e.preventDefault();
197 _this.trigger = $(e.currentTarget);
198 _this.show();
199 };
200 $('.t3js-image-manipulation-trigger').off('click').click(triggerHandler);
201 };
202 /**
203 * @method initializeCropperModal
204 * @desc Initialize the cropper modal and dispatch the cropper init
205 * @private
206 */
207 ImageManipulation.prototype.initializeCropperModal = function () {
208 var _this = this;
209 var image = this.currentModal.find(this.cropImageSelector);
210 ImagesLoaded(image, function () {
211 setTimeout(function () {
212 _this.init();
213 }, 100);
214 });
215 };
216 /**
217 * @method show
218 * @desc Load the image and setup the modal UI
219 * @private
220 */
221 ImageManipulation.prototype.show = function () {
222 var _this = this;
223 var modalTitle = this.trigger.data('modalTitle');
224 var buttonPreviewText = this.trigger.data('buttonPreviewText');
225 var buttonDismissText = this.trigger.data('buttonDismissText');
226 var buttonSaveText = this.trigger.data('buttonSaveText');
227 var imageUri = this.trigger.data('url');
228 var initCropperModal = this.initializeCropperModal.bind(this);
229 /**
230 * Open modal with image to crop
231 */
232 this.currentModal = Modal.advanced({
233 additionalCssClasses: ['modal-image-manipulation'],
234 ajaxCallback: initCropperModal,
235 buttons: [
236 {
237 btnClass: 'btn-default pull-left',
238 dataAttributes: {
239 method: 'preview',
240 },
241 icon: 'actions-view',
242 text: buttonPreviewText,
243 },
244 {
245 btnClass: 'btn-default',
246 dataAttributes: {
247 method: 'dismiss',
248 },
249 icon: 'actions-close',
250 text: buttonDismissText,
251 },
252 {
253 btnClass: 'btn-primary',
254 dataAttributes: {
255 method: 'save',
256 },
257 icon: 'actions-document-save',
258 text: buttonSaveText,
259 },
260 ],
261 callback: function (currentModal) {
262 currentModal.find('.t3js-modal-body')
263 .addClass('cropper');
264 },
265 content: imageUri,
266 size: Modal.sizes.full,
267 style: Modal.styles.dark,
268 title: modalTitle,
269 type: 'ajax',
270 });
271 this.currentModal.on('hide.bs.modal', function (e) {
272 _this.destroy();
273 });
274 // Do not dismiss the modal when clicking beside it to avoid data loss
275 this.currentModal.data('bs.modal').options.backdrop = 'static';
276 };
277 /**
278 * @method init
279 * @desc Initializes the cropper UI and sets up all the event indings for the UI
280 * @private
281 */
282 ImageManipulation.prototype.init = function () {
283 var _this = this;
284 var image = this.currentModal.find(this.cropImageSelector);
285 var imageHeight = $(image).height();
286 var imageWidth = $(image).width();
287 var data = this.trigger.attr('data-crop-variants');
288 if (!data) {
289 throw new TypeError('ImageManipulation: No cropVariants data found for image');
290 }
291 // If we have data already set we assume an internal reinit eg. after resizing
292 this.data = $.isEmptyObject(this.data) ? JSON.parse(data) : this.data;
293 // Initialize our class members
294 this.currentModal.find(this.cropImageContainerSelector).css({ height: imageHeight, width: imageWidth });
295 this.cropVariantTriggers = this.currentModal.find('.t3js-crop-variant-trigger');
296 this.activeCropVariantTrigger = this.currentModal.find('.t3js-crop-variant-trigger.is-active');
297 this.cropInfo = this.currentModal.find(this.cropInfoSelector);
298 this.saveButton = this.currentModal.find('[data-method=save]');
299 this.previewButton = this.currentModal.find('[data-method=preview]');
300 this.dismissButton = this.currentModal.find('[data-method=dismiss]');
301 this.resetButton = this.currentModal.find('[data-method=reset]');
302 this.cropperCanvas = this.currentModal.find('#js-crop-canvas');
303 this.aspectRatioTrigger = this.currentModal.find('[data-method=setAspectRatio]');
304 this.currentCropVariant = this.data[this.activeCropVariantTrigger.attr('data-crop-variant-id')];
305 /**
306 * Assign EventListener to cropVariantTriggers
307 */
308 this.cropVariantTriggers.off('click').on('click', function (e) {
309 /**
310 * Is the current cropVariantTrigger is active, bail out.
311 * Bootstrap doesn't provide this functionality when collapsing the Collaps panels
312 */
313 if ($(e.currentTarget).hasClass('is-active')) {
314 e.stopPropagation();
315 e.preventDefault();
316 return;
317 }
318 _this.activeCropVariantTrigger.removeClass('is-active');
319 $(e.currentTarget).addClass('is-active');
320 _this.activeCropVariantTrigger = $(e.currentTarget);
321 var cropVariant = _this.data[_this.activeCropVariantTrigger.attr('data-crop-variant-id')];
322 var imageData = _this.cropper.cropper('getImageData');
323 cropVariant.cropArea = _this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
324 _this.currentCropVariant = $.extend(true, {}, cropVariant);
325 _this.update(cropVariant);
326 });
327 /**
328 * Assign EventListener to aspectRatioTrigger
329 */
330 this.aspectRatioTrigger.off('click').on('click', function (e) {
331 var ratioId = $(e.currentTarget).attr('data-option');
332 var temp = $.extend(true, {}, _this.currentCropVariant);
333 var ratio = temp.allowedAspectRatios[ratioId];
334 _this.setAspectRatio(ratio);
335 // Set data explicitly or setAspectRatio upscales the crop
336 _this.setCropArea(temp.cropArea);
337 _this.currentCropVariant = $.extend(true, {}, temp, { selectedRatio: ratioId });
338 _this.update(_this.currentCropVariant);
339 });
340 /**
341 * Assign EventListener to saveButton
342 */
343 this.saveButton.off('click').on('click', function () {
344 _this.save(_this.data);
345 });
346 /**
347 * Assign EventListener to previewButton if preview url exists
348 */
349 if (this.trigger.attr('data-preview-url')) {
350 this.previewButton.off('click').on('click', function () {
351 _this.openPreview(_this.data);
352 });
353 }
354 else {
355 this.previewButton.hide();
356 }
357 /**
358 * Assign EventListener to dismissButton
359 */
360 this.dismissButton.off('click').on('click', function () {
361 _this.currentModal.modal('hide');
362 });
363 /**
364 * Assign EventListener to resetButton
365 */
366 this.resetButton.off('click').on('click', function (e) {
367 var imageData = _this.cropper.cropper('getImageData');
368 var resetCropVariantString = $(e.currentTarget).attr('data-crop-variant');
369 e.preventDefault();
370 e.stopPropagation();
371 if (!resetCropVariantString) {
372 throw new TypeError('TYPO3 Cropper: No cropVariant data attribute found on reset element.');
373 }
374 var resetCropVariant = JSON.parse(resetCropVariantString);
375 var absoluteCropArea = _this.convertRelativeToAbsoluteCropArea(resetCropVariant.cropArea, imageData);
376 _this.currentCropVariant = $.extend(true, {}, resetCropVariant, { cropArea: absoluteCropArea });
377 _this.update(_this.currentCropVariant);
378 });
379 // If we start without an cropArea, maximize the cropper
380 if (ImageManipulation.isEmptyArea(this.currentCropVariant.cropArea)) {
381 this.defaultOpts = $.extend({
382 autoCropArea: 1,
383 }, this.defaultOpts);
384 }
385 /**
386 * Initialise the cropper
387 *
388 * Note: We use the extraneous jQuery object here, as CropperJS won't work inside the <iframe>
389 * The top.require is now inlined @see ImageManipulationElemen.php:143
390 * TODO: Find a better solution for cross iframe communications
391 */
392 this.cropper = top.$(image).cropper($.extend(this.defaultOpts, {
393 built: this.cropBuiltHandler,
394 crop: this.cropMoveHandler,
395 cropend: this.cropEndHandler,
396 cropstart: this.cropStartHandler,
397 data: this.currentCropVariant.cropArea,
398 }));
399 };
400 /**
401 * @method update
402 * @desc Update current cropArea position and size when changing cropVariants
403 * @param {CropVariant} cropVariant - The new cropVariant to update the UI with
404 */
405 ImageManipulation.prototype.update = function (cropVariant) {
406 var temp = $.extend(true, {}, cropVariant);
407 var selectedRatio = cropVariant.allowedAspectRatios[cropVariant.selectedRatio];
408 this.currentModal.find('[data-option]').removeClass('active');
409 this.currentModal.find("[data-option=\"" + cropVariant.selectedRatio + "\"]").addClass('active');
410 /**
411 * Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
412 */
413 this.setAspectRatio(selectedRatio);
414 this.setCropArea(temp.cropArea);
415 this.currentCropVariant = $.extend(true, {}, temp, cropVariant);
416 this.cropBox.find(this.coverAreaSelector).remove();
417 // If the current container has a focus area element, deregister and cleanup prior to initialization
418 if (this.cropBox.has(this.focusAreaSelector).length) {
419 this.focusArea.resizable('destroy').draggable('destroy');
420 this.focusArea.remove();
421 }
422 // Check if new cropVariant has focusArea
423 if (cropVariant.focusArea) {
424 // Init or reinit focusArea
425 if (ImageManipulation.isEmptyArea(cropVariant.focusArea)) {
426 this.currentCropVariant.focusArea = $.extend(true, {}, this.defaultFocusArea);
427 }
428 this.initFocusArea(this.cropBox);
429 this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea);
430 }
431 // Check if new cropVariant has coverAreas
432 if (cropVariant.coverAreas) {
433 // Init or reinit focusArea
434 this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
435 }
436 this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
437 };
438 /**
439 * @method initFocusArea
440 * @desc Initializes the focus area inside a container and registers the resizable and draggable interfaces to it
441 * @param {JQuery} container
442 * @private
443 */
444 ImageManipulation.prototype.initFocusArea = function (container) {
445 var _this = this;
446 this.focusArea = $('<div id="t3js-cropper-focus-area" class="cropper-focus-area"></div>');
447 container.append(this.focusArea);
448 this.focusArea
449 .draggable({
450 containment: container,
451 create: function () {
452 _this.scaleAndMoveFocusArea(_this.currentCropVariant.focusArea);
453 },
454 drag: function () {
455 var _a = container.offset(), left = _a.left, top = _a.top;
456 var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
457 var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
458 focusArea.x = (fLeft - left) / container.width();
459 focusArea.y = (fTop - top) / container.height();
460 _this.updatePreviewThumbnail(_this.currentCropVariant, _this.activeCropVariantTrigger);
461 if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
462 _this.focusArea.addClass('has-nodrop');
463 }
464 else {
465 _this.focusArea.removeClass('has-nodrop');
466 }
467 },
468 revert: function () {
469 var revertDelay = 250;
470 var _a = container.offset(), left = _a.left, top = _a.top;
471 var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
472 var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
473 if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
474 _this.focusArea.removeClass('has-nodrop');
475 ImageManipulation.wait(function () {
476 focusArea.x = (fLeft - left) / container.width();
477 focusArea.y = (fTop - top) / container.height();
478 _this.updateCropVariantData(_this.currentCropVariant);
479 }, revertDelay);
480 return true;
481 }
482 return false;
483 },
484 revertDuration: 200,
485 stop: function () {
486 var _a = container.offset(), left = _a.left, top = _a.top;
487 var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
488 var focusArea = _this.currentCropVariant.focusArea;
489 focusArea.x = (fLeft - left) / container.width();
490 focusArea.y = (fTop - top) / container.height();
491 _this.scaleAndMoveFocusArea(focusArea);
492 },
493 })
494 .resizable({
495 containment: container,
496 handles: 'all',
497 resize: function () {
498 var _a = container.offset(), left = _a.left, top = _a.top;
499 var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
500 var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
501 focusArea.height = _this.focusArea.height() / container.height();
502 focusArea.width = _this.focusArea.width() / container.width();
503 focusArea.x = (fLeft - left) / container.width();
504 focusArea.y = (fTop - top) / container.height();
505 _this.updatePreviewThumbnail(_this.currentCropVariant, _this.activeCropVariantTrigger);
506 if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
507 _this.focusArea.addClass('has-nodrop');
508 }
509 else {
510 _this.focusArea.removeClass('has-nodrop');
511 }
512 },
513 stop: function (event, ui) {
514 var revertDelay = 250;
515 var _a = container.offset(), left = _a.left, top = _a.top;
516 var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
517 var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
518 if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
519 ui.element.animate($.extend(ui.originalPosition, ui.originalSize), revertDelay, function () {
520 focusArea.height = _this.focusArea.height() / container.height();
521 focusArea.height = _this.focusArea.height() / container.height();
522 focusArea.width = _this.focusArea.width() / container.width();
523 focusArea.x = (fLeft - left) / container.width();
524 focusArea.y = (fTop - top) / container.height();
525 _this.scaleAndMoveFocusArea(focusArea);
526 _this.focusArea.removeClass('has-nodrop');
527 });
528 }
529 else {
530 _this.scaleAndMoveFocusArea(focusArea);
531 }
532 },
533 });
534 };
535 /**
536 * @method initCoverAreas
537 * @desc Initialise cover areas inside the cropper container
538 * @param {JQuery} container - The container element to append the cover areas
539 * @param {Array<Area>} coverAreas - An array of areas to construxt the cover area elements from
540 */
541 ImageManipulation.prototype.initCoverAreas = function (container, coverAreas) {
542 coverAreas.forEach(function (coverArea) {
543 var coverAreaCanvas = $('<div class="cropper-cover-area t3js-cropper-cover-area"></div>');
544 container.append(coverAreaCanvas);
545 coverAreaCanvas.css({
546 height: ImageManipulation.toCssPercent(coverArea.height),
547 left: ImageManipulation.toCssPercent(coverArea.x),
548 top: ImageManipulation.toCssPercent(coverArea.y),
549 width: ImageManipulation.toCssPercent(coverArea.width),
550 });
551 });
552 };
553 /**
554 * @method updatePreviewThumbnail
555 * @desc Sync the croping (and focus area) to the preview thumbnail
556 * @param {CropVariant} cropVariant - The crop variant to preview in the thumbnail
557 * @param {JQuery} cropVariantTrigger - The crop variant element containing the thumbnail
558 * @private
559 */
560 ImageManipulation.prototype.updatePreviewThumbnail = function (cropVariant, cropVariantTrigger) {
561 var styles;
562 var cropperPreviewThumbnailCrop = cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
563 var cropperPreviewThumbnailImage = cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
564 var cropperPreviewThumbnailFocus = cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
565 var imageData = this.cropper.cropper('getImageData');
566 // Update the position/dimension of the crop area in the preview
567 cropperPreviewThumbnailCrop.css({
568 height: ImageManipulation.toCssPercent(cropVariant.cropArea.height / imageData.naturalHeight),
569 left: ImageManipulation.toCssPercent(cropVariant.cropArea.x / imageData.naturalWidth),
570 top: ImageManipulation.toCssPercent(cropVariant.cropArea.y / imageData.naturalHeight),
571 width: ImageManipulation.toCssPercent(cropVariant.cropArea.width / imageData.naturalWidth),
572 });
573 // Show and update focusArea in the preview only if we really have one configured
574 if (cropVariant.focusArea) {
575 cropperPreviewThumbnailFocus.css({
576 height: ImageManipulation.toCssPercent(cropVariant.focusArea.height),
577 left: ImageManipulation.toCssPercent(cropVariant.focusArea.x),
578 top: ImageManipulation.toCssPercent(cropVariant.focusArea.y),
579 width: ImageManipulation.toCssPercent(cropVariant.focusArea.width),
580 });
581 }
582 // Destruct the preview container's CSS properties
583 styles = cropperPreviewThumbnailCrop.css([
584 'width', 'height', 'left', 'top',
585 ]);
586 /**
587 * Apply negative margins on the previewThumbnailImage to make the illusion of an offset
588 */
589 cropperPreviewThumbnailImage.css({
590 height: parseFloat(styles.height) * (1 / (cropVariant.cropArea.height / imageData.naturalHeight)) + "px",
591 margin: -1 * parseFloat(styles.left) + "px",
592 marginTop: -1 * parseFloat(styles.top) + "px",
593 width: parseFloat(styles.width) * (1 / (cropVariant.cropArea.width / imageData.naturalWidth)) + "px",
594 });
595 };
596 /**
597 * @method scaleAndMoveFocusArea
598 * @desc Calculation logic for moving the focus area given the
599 * specified constrains of a crop and an optional cover area
600 * @param {Area} focusArea - The translation data
601 */
602 ImageManipulation.prototype.scaleAndMoveFocusArea = function (focusArea) {
603 this.focusArea.css({
604 height: ImageManipulation.toCssPercent(focusArea.height),
605 left: ImageManipulation.toCssPercent(focusArea.x),
606 top: ImageManipulation.toCssPercent(focusArea.y),
607 width: ImageManipulation.toCssPercent(focusArea.width),
608 });
609 this.currentCropVariant.focusArea = focusArea;
610 this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
611 this.updateCropVariantData(this.currentCropVariant);
612 };
613 /**
614 * @method updateCropVariantData
615 * @desc Immutably updates the currently selected cropVariant data
616 * @param {CropVariant} currentCropVariant - The cropVariant to immutably save
617 * @private
618 */
619 ImageManipulation.prototype.updateCropVariantData = function (currentCropVariant) {
620 var imageData = this.cropper.cropper('getImageData');
621 var absoluteCropArea = this.convertAbsoluteToRelativeCropArea(currentCropVariant.cropArea, imageData);
622 this.data[currentCropVariant.id] = $.extend(true, {}, currentCropVariant, { cropArea: absoluteCropArea });
623 };
624 /**
625 * @method setAspectRatio
626 * @desc Sets the cropper to a specific ratio
627 * @param {ratio} ratio - The ratio value to apply
628 * @private
629 */
630 ImageManipulation.prototype.setAspectRatio = function (ratio) {
631 this.cropper.cropper('setAspectRatio', ratio.value);
632 };
633 /**
634 * @method setCropArea
635 * @desc Sets the cropper to a specific crop area
636 * @param {cropArea} cropArea - The crop area to apply
637 * @private
638 */
639 ImageManipulation.prototype.setCropArea = function (cropArea) {
640 var currentRatio = this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];
641 if (currentRatio.value === 0) {
642 this.cropper.cropper('setData', {
643 height: cropArea.height,
644 width: cropArea.width,
645 x: cropArea.x,
646 y: cropArea.y,
647 });
648 }
649 else {
650 this.cropper.cropper('setData', {
651 height: cropArea.height,
652 x: cropArea.x,
653 y: cropArea.y,
654 });
655 }
656 };
657 /**
658 * @method checkFocusAndCoverAreas
659 * @desc Checks is one focus area and one or more cover areas overlap
660 * @param focusArea
661 * @param coverAreas
662 * @return {boolean}
663 */
664 ImageManipulation.prototype.checkFocusAndCoverAreasCollision = function (focusArea, coverAreas) {
665 if (!coverAreas) {
666 return false;
667 }
668 return coverAreas
669 .some(function (coverArea) {
670 // noinspection OverlyComplexBooleanExpressionJS
671 return (focusArea.x < coverArea.x + coverArea.width &&
672 focusArea.x + focusArea.width > coverArea.x &&
673 focusArea.y < coverArea.y + coverArea.height &&
674 focusArea.height + focusArea.y > coverArea.y);
675 });
676 };
677 /**
678 * @method convertAbsoluteToRelativeCropArea
679 * @desc Converts a crop area from absolute pixel-based into relative length values
680 * @param {Area} cropArea - The crop area to convert from
681 * @param {CropperImageData} imageData - The image data
682 * @return {Area}
683 */
684 ImageManipulation.prototype.convertAbsoluteToRelativeCropArea = function (cropArea, imageData) {
685 var height = cropArea.height, width = cropArea.width, x = cropArea.x, y = cropArea.y;
686 return {
687 height: height / imageData.naturalHeight,
688 width: width / imageData.naturalWidth,
689 x: x / imageData.naturalWidth,
690 y: y / imageData.naturalHeight,
691 };
692 };
693 /**
694 * @method convertRelativeToAbsoluteCropArea
695 * @desc Converts a crop area from relative into absolute pixel-based length values
696 * @param {Area} cropArea - The crop area to convert from
697 * @param {CropperImageData} imageData - The image data
698 * @return {{height: number, width: number, x: number, y: number}}
699 */
700 ImageManipulation.prototype.convertRelativeToAbsoluteCropArea = function (cropArea, imageData) {
701 var height = cropArea.height, width = cropArea.width, x = cropArea.x, y = cropArea.y;
702 return {
703 height: height * imageData.naturalHeight,
704 width: width * imageData.naturalWidth,
705 x: x * imageData.naturalWidth,
706 y: y * imageData.naturalHeight,
707 };
708 };
709 /**
710 * @method setPreviewImages
711 * @desc Updates the preview images in the editing section with the respective crop variants
712 * @param {Object} data - The internal crop variants state
713 */
714 ImageManipulation.prototype.setPreviewImages = function (data) {
715 var _this = this;
716 var $image = this.cropper;
717 var imageData = $image.cropper('getImageData');
718 // Iterate over the crop variants and set up their respective preview
719 Object.keys(data).forEach(function (cropVariantId) {
720 var cropVariant = data[cropVariantId];
721 var cropData = _this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
722 var $preview = _this.trigger
723 .closest('.form-group')
724 .find(".t3js-image-manipulation-preview[data-crop-variant-id=\"" + cropVariantId + "\"]");
725 var $previewSelectedRatio = _this.trigger
726 .closest('.form-group')
727 .find(".t3js-image-manipulation-selected-ratio[data-crop-variant-id=\"" + cropVariantId + "\"]"); // tslint:disable-line:max-line-length
728 if ($preview.length === 0) {
729 return;
730 }
731 var previewWidth = $preview.width();
732 var previewHeight = $preview.data('preview-height');
733 // Adjust aspect ratio of preview width/height
734 var aspectRatio = cropData.width / cropData.height;
735 var tmpHeight = previewWidth / aspectRatio;
736 if (tmpHeight > previewHeight) {
737 previewWidth = previewHeight * aspectRatio;
738 }
739 else {
740 previewHeight = tmpHeight;
741 }
742 // preview should never be up-scaled
743 if (previewWidth > cropData.width) {
744 previewWidth = cropData.width;
745 previewHeight = cropData.height;
746 }
747 var ratio = previewWidth / cropData.width;
748 var $viewBox = $('<div />').html('<img src="' + $image.attr('src') + '">');
749 var $ratioTitleText = _this.currentModal.find(".t3-js-ratio-title[data-ratio-id=\"" + cropVariant.id + cropVariant.selectedRatio + "\"]"); // tslint:disable-line:max-line-length
750 $previewSelectedRatio.text($ratioTitleText.text());
751 $viewBox.addClass('cropper-preview-container');
752 $preview.empty().append($viewBox);
753 $viewBox.wrap('<span class="thumbnail thumbnail-status"></span>');
754 $viewBox.width(previewWidth).height(previewHeight).find('img').css({
755 height: imageData.naturalHeight * ratio,
756 left: -cropData.x * ratio,
757 top: -cropData.y * ratio,
758 width: imageData.naturalWidth * ratio,
759 });
760 });
761 };
762 /**
763 * @method openPreview
764 * @desc Opens a preview view with the crop variants
765 * @param {object} data - The whole data object containing all the cropVariants
766 * @private
767 */
768 ImageManipulation.prototype.openPreview = function (data) {
769 var cropVariants = ImageManipulation.serializeCropVariants(data);
770 var previewUrl = this.trigger.attr('data-preview-url');
771 previewUrl = previewUrl + '&cropVariants=' + encodeURIComponent(cropVariants);
772 window.open(previewUrl, 'TYPO3ImageManipulationPreview');
773 };
774 /**
775 * @method save
776 * @desc Saves the edited cropVariants to a hidden field
777 * @param {object} data - The whole data object containing all the cropVariants
778 * @private
779 */
780 ImageManipulation.prototype.save = function (data) {
781 var cropVariants = ImageManipulation.serializeCropVariants(data);
782 var hiddenField = $("#" + this.trigger.attr('data-field'));
783 this.trigger.attr('data-crop-variants', JSON.stringify(data));
784 this.setPreviewImages(data);
785 hiddenField.val(cropVariants);
786 this.currentModal.modal('hide');
787 };
788 /**
789 * @method destroy
790 * @desc Destroy the ImageManipulation including cropper and alike
791 * @private
792 */
793 ImageManipulation.prototype.destroy = function () {
794 if (this.currentModal) {
795 this.cropper.cropper('destroy');
796 this.cropper = null;
797 this.currentModal = null;
798 this.data = null;
799 }
800 };
801 /**
802 * @method resizeEnd
803 * @desc Calls a function when the cropper has been resized
804 * @param {Function} fn - The function to call on resize completion
805 * @private
806 */
807 ImageManipulation.prototype.resizeEnd = function (fn) {
808 var _this = this;
809 var timer;
810 $(window).on('resize', function () {
811 clearTimeout(timer);
812 timer = setTimeout(function () {
813 fn();
814 }, _this.resizeTimeout);
815 });
816 };
817 return ImageManipulation;
818 }());
819 return new ImageManipulation();
820 });