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