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