[FEATURE] Add TypeScript and CSS for new imageManipulation 65/51565/6
authorRaphael Okon <raphael@okon.io>
Tue, 7 Feb 2017 17:41:55 +0000 (18:41 +0100)
committerAndreas Fernandez <typo3@scripting-base.de>
Tue, 7 Feb 2017 20:26:12 +0000 (21:26 +0100)
This adds the necessary TypeScript and CSS changes
for the new imageManipulation element.

Resolves: #75880
Releases: master
Change-Id: I56c5042c6e6de7141ba59014ab31051c5c9cc34f
Reviewed-on: https://review.typo3.org/51565
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
Build/Resources/Public/Less/TYPO3/_element_cropper.less
Build/Resources/Public/Less/TYPO3/_element_modal.less
Build/Resources/Public/Less/cropper/variables.less
Build/tsconfig.json
Build/tslint.json
typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/Css/backend.css
typo3/sysext/backend/Resources/Public/Images/cropper-background-cover-area.svg [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/ImageManipulation.js
typo3/sysext/form/Resources/Public/Css/form.css
typo3/sysext/install/Resources/Public/Css/install.css

index 86630f9..e63faaa 100644 (file)
-.cropper-line {
-       background-color: #FFFFFF;
-}
-.cropper-point {
-       background-color: #FFFFFF;
-}
-.cropper-point.point-se:before {
-       background-color: #FFFFFF;
-}
-.cropper-view-box {
-       outline: 1px dashed #6699ff;
-       outline-color: rgb(255, 255, 255, 0.75);
-}
-.cropper-bg {
-       background-image: data-uri("../Images/cropper-background.png");
+.cropper {
+       .cropper-line {
+               background-color: transparent;
+       }
+
+       .cropper-dashed {
+               border: 1px dashed @color-orange;
+       }
+
+       .cropper-point {
+               background-color: @color-orange;
+
+               &.point-nw {
+                       left: 0; /* 3 */
+                       top: 0; /* 3 */
+               }
+
+               &.point-w {
+                       left: 0; /* 3 */
+               }
+
+               &.point-sw {
+                       left: 0; /* 3 */
+                       bottom: 0; /* 3 */
+               }
+
+               &.point-ne {
+                       right: 0; /* 3 */
+                       top: 0; /* 3 */
+               }
+
+               &.point-e {
+                       right: 0; /* 3 */
+               }
+
+               &.point-se {
+                       right: 0; /* 3 */
+                       bottom: 0; /* 3 */
+
+                       &:before {
+                               background-color: @color-white;
+                       }
+               }
+
+               &.point-n {
+                       top: 0; /* 3 */
+               }
+
+               &.point-s {
+                       bottom: 0; /* 3 */
+               }
+       }
+
+       .cropper-view-box {
+               outline: 1px dashed @color-orange;
+       }
+
+       .cropper-bg {
+               background-image: data-uri("../Images/cropper-background.png");
+       }
+
+       .cropper-image-container {
+               height: 100% !important;
+               width: 100% !important;
+               max-width: 1000px !important;
+       }
+
+       .ratio-buttons {
+               .btn.btn-default {
+                       margin-bottom: 5px;
+               }
+       }
+
+       // Cropper style tweaks for Bootstrap .panel
+       .panel-group {
+               [aria-expanded=true] {
+                       border-left: 2px solid @color-orange;
+                       position: relative;
+
+                       &:before {
+                               position: absolute;
+                               left: -10px;
+                               top: 50%;
+                               margin-top: -10px;
+                               content: " ";
+                               width: 0;
+                               height: 0;
+                               border-style: solid;
+                               border-width: 10px 10px 10px 0;
+                               border-color: transparent @color-orange transparent transparent;
+                       }
+
+                       &[data-toggle=collapse] {
+                               background-color: #333;
+                       }
+               }
+
+               [aria-expanded=false] {
+                       border-left: 2px solid #444;
+                       position: relative;
+               }
+
+               table, label {
+                       color: @color-white;
+               }
+
+               position: relative;
+               margin: -15px;
+       }
+
+       .panel-collapse.collapse {
+               background-color: #2c2c2c !important;
+               border-left: 2px solid @color-orange;
+       }
+
+       .panel-heading {
+               padding: 0;
+
+               .panel-title {
+                       > [data-crop-variant] {
+                               display: flex;
+                               padding: 10px 15px;
+                               justify-content: space-between;
+                       }
+
+                       > a {
+                               text-decoration: none !important;
+                               user-select: none;
+                               font-size: 13px;
+                       }
+               }
+       }
+
+       .panel-default > .panel-heading + .panel-collapse > .panel-body {
+               border-top-color: #333;
+       }
+
+       .panel-group,
+       .panel-default .panel-heading,
+       .panel {
+               background: #222222;
+               border: none;
+               color: @color-white;
+       }
+
+       .btn-secondary {
+               color: @color-white;
+               background-color: #777;
+               border-color: #555;
+
+               &:hover,
+               &:focus,
+               &:active,
+               &.active {
+                       color: @color-white;
+                       background-color: #888;
+                       border-color: #444;
+               }
+       }
+
+       .open .dropdown-toggle.btn-secondary {
+               &:extend(.cropper .btn-secondary);
+       }
+
+       /**
+        * Style overrides for CropperJS CSS and additional styles to make cropper look like expected
+        *
+        * 1. Fixes hidden drag/resize handles for cropper
+        * 2. Fixes containment on the jQuery UI resizeable
+        * 3. Offset cropper lines and handles to inside of container
+        */
+       .cropper-container.cropper-bg {
+               overflow: visible; /* 1 */
+       }
+
+       .cropper-crop-box {
+               overflow: hidden; /* 2 */
+
+               &:after {
+                       background-color: @color-orange;
+                       content: "Cropped area";
+                       position: absolute;
+                       left: 0;
+                       top: 0;
+                       font-size: 10px;
+                       color: black;
+                       height: 16px;
+                       width: 100%;
+                       max-width: 80px;
+                       text-overflow: ellipsis;
+                       white-space: nowrap;
+                       padding: 0 4px;
+                       pointer-events: none;
+                       overflow: hidden;
+               }
+       }
+
+
+       .cropper-line {
+               &.line-w {
+                       left: 0; /* 3 */
+               }
+
+               &.line-e {
+                       right: 0; /* 3 */
+               }
+
+               &.line-n {
+                       top: 0; /* 3 */
+               }
+
+               &.line-s {
+                       bottom: 0; /* 3 */
+               }
+       }
+
+       // Style overrides for jQueryUI
+       .ui-resizable-handle {
+               &.ui-resizable-n,
+               &.ui-resizable-s,
+               &.ui-resizable-e,
+               &.ui-resizable-w {
+                       border-color: transparent;
+                       transform: none;
+               }
+
+               &.ui-resizable-e,
+               &.ui-resizable-w {
+                       width: 6px;
+               }
+
+               &.ui-resizable-n,
+               &.ui-resizable-s {
+                       height: 6px;
+               }
+
+               &.ui-resizable-e {
+                       right: 0;
+               }
+
+               &.ui-resizable-w {
+                       left: 0;
+               }
+
+               &.ui-resizable-n {
+                       top: 0;
+               }
+
+               &.ui-resizable-s {
+                       bottom: 0;
+               }
+
+               &.ui-resizable-sw,
+               &.ui-resizable-se,
+               &.ui-resizable-ne,
+               &.ui-resizable-nw {
+                       transform: none;
+                       background-color: #ccc;
+                       height: 6px;
+                       width: 6px;
+               }
+
+               &.ui-resizable-nw {
+                       top: 0;
+                       left: 0;
+               }
+
+               &.ui-resizable-ne {
+                       top: 0;
+                       right: 0;
+               }
+
+               &.ui-resizable-se {
+                       bottom: 0;
+                       right: 0;
+               }
+
+               &.ui-resizable-sw {
+                       bottom: 0;
+                       left: 0;
+               }
+       }
+
+       // Custom styles for cropper radio buttons
+       .t3js-ratio-buttons {
+               margin-bottom: 10px;
+
+               .btn:not(.active) .fa {
+                       display: none;
+               }
+       }
+
+
+       // Cropper UI-specific styles
+       .cropper-focus-area {
+               cursor: move;
+               height: 200px;
+               width: 200px;
+               background-color: rgba(215, 187, 0, .5);
+               position: absolute;
+               z-index: 999999;
+               opacity: 1;
+               overflow: hidden;
+               transition: background-color 300ms;
+
+               &.has-nodrop,
+               &.has-nodrop:hover {
+                       background-color: rgba(211, 35, 46, .6) !important;
+                       transition: background-color 300ms;
+               }
+
+               &:hover,
+               &:focus {
+                       background-color: rgba(215, 187, 0, .7);
+               }
+
+               &:after {
+                       background-color: rgba(255, 255, 255, .95);
+                       content: "Focus";
+                       position: absolute;
+                       left: 0;
+                       top: 0;
+                       font-size: 10px;
+                       color: black;
+                       height: 16px;
+                       width: 100%;
+                       max-width: 44px;
+                       text-overflow: ellipsis;
+                       white-space: nowrap;
+                       padding: 0 4px 0 8px; /* Additional 4px on left due to resize handle on focus area */
+                       pointer-events: none;
+                       overflow: hidden;
+               }
+       }
+
+       .t3js-cropper-cover-area {
+               background: url("../Images/cropper-background-cover-area.svg");
+               pointer-events: none;
+               cursor: not-allowed;
+               position: absolute;
+               opacity: 1;
+               z-index: 99999;
+
+               &:after {
+                       background-color: rgba(255, 255, 255, .95);
+                       content: "Cover area";
+                       position: absolute;
+                       left: 0;
+                       top: 0;
+                       font-size: 10px;
+                       color: black;
+                       height: 16px;
+                       width: 100%;
+                       max-width: 80px;
+                       text-overflow: ellipsis;
+                       white-space: nowrap;
+                       padding: 0 4px;
+                       pointer-events: none;
+                       overflow: hidden;
+               }
+       }
+
+       .cropper-preview-thumbnail {
+               -webkit-user-select: none;
+               -moz-user-select: none;
+               -ms-user-select: none;
+               user-select: none;
+               position: relative;
+               max-width: 100px;
+               max-height: 100px;
+               overflow: hidden;
+
+               &:after {
+                       background-color: rgba(0, 0, 0, .5);
+                       content: " ";
+                       top: 0;
+                       left: 0;
+                       bottom: 0;
+                       right: 0;
+                       position: absolute;
+                       z-index: 9;
+               }
+
+               &.wide {
+                       width: 100px;
+                       height: auto;
+               }
+
+               &.tall {
+                       width: auto;
+                       height: 80px;
+               }
+       }
+
+       .cropper-preview-thumbnail-image {
+               left: 0;
+               top: 0;
+       }
+
+       .wide .cropper-preview-thumbnail-image {
+               width: 100%;
+               height: auto;
+       }
+
+       .tall .cropper-preview-thumbnail-image {
+               width: auto;
+               height: 100%;
+       }
+
+       .cropper-preview-thumbnail-crop-area {
+               border: 1px solid @color-orange;
+               position: absolute;
+               z-index: 10;
+               overflow: hidden;
+       }
+
+       .cropper-preview-thumbnail-focus-area {
+               background-color: rgba(215, 187, 0, .7);
+               position: absolute;
+               z-index: 11;
+       }
 }
-.cropper-image-container {
 
+:root .cropper-preview-thumbnail-crop-image {
+       image-orientation: 0deg;
+       display: block;
+       height: 100%;
+       width: 100%;
+       min-width: 0;
+       max-width: none;
+       min-height: 0;
+       max-height: none;
 }
+
 .cropper-preview-container {
        overflow: hidden;
        position: relative;
                display: block;
                position: absolute;
                width: 100%;
-               min-width: 0!important;
-               min-height: 0!important;
-               max-width: none!important;
-               max-height: none!important;
+               min-width: 0 !important;
+               min-height: 0 !important;
+               max-width: none !important;
+               max-height: none !important;
        }
-}
-.ratio-buttons {
-       .btn.btn-default {
-               margin-bottom: 5px;
-       }
-}
+}
\ No newline at end of file
index 5e1b421..c0c2066 100644 (file)
        }
 }
 
+/**
+ * Cropper modal component styles
+ */
 .modal-image-manipulation {
+       width: 90vw;
+       height: 90vh;
+       .modal-panel {
+               display: flex;
+               max-height: 80vh;
+               overflow: visible;
+       }
+       .modal-panel-body {
+               background-image: data-uri("../Images/cropper-background.png");
+               display: flex;
+               align-items: center;
+               justify-content: center;
+       }
+       .modal-panel .modal-panel-body {
+               max-height: 100%;
+               width: 100% !important;
+       }
+
+       .modal-panel .modal-panel-sidebar {
+               min-width: 300px;
+               max-height: 100%;
+               overflow: auto;
+               -webkit-overflow-scrolling: touch;
+       }
+
+       .modal-panel .modal-panel-sidebar-right {
+               background-color: #212424;
+               position: relative;
+       }
+
+       .modal-panel .modal-footer {
+               position: absolute;
+               bottom: 0;
+               right: 0;
+               width: 100%;
+       }
        .modal-body {
                .col-lg-12 {
                        padding-right: 450px;
                color: #FFF;
        }
        .modal-footer {
-               border-top: none;
+               border-top: 1px solid #000000;
        }
 }
 
                        max-width: 100%;
                }
        }
-}
\ No newline at end of file
+}
index d2189c5..830fe21 100644 (file)
@@ -8,6 +8,7 @@
 @color-brand: #69f;
 @color-black: #000;
 @color-white: #fff;
+@color-orange: #ff8700;
 
 
 // Media queries breakpoints
index 8bf3498..0aa6ead 100644 (file)
     ],
     "files": [
         "../typo3/sysext/backend/Resources/Private/TypeScript/ColorPicker.ts",
-        "../typo3/sysext/backend/Resources/Private/TypeScript/FormEngineReview.ts"
+        "../typo3/sysext/backend/Resources/Private/TypeScript/FormEngineReview.ts",
+        "../typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts"
     ]
-}
\ No newline at end of file
+}
index 208d322..362d261 100644 (file)
@@ -4,6 +4,7 @@
        "rules": {
                "interface-name": false,
                "no-console": false,
+               "no-namespace": [false, "allow-declarations"],
                "quotemark": [true, "single", "avoid-escape"],
                "typedef": [true, "call-signature", "arrow-call-signature", "arrow-parameter", "property-declaration", "variable-declaration", "parameter", "member-variable-declaration"]
        }
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts b/typo3/sysext/backend/Resources/Private/TypeScript/ImageManipulation.ts
new file mode 100644 (file)
index 0000000..6bad4de
--- /dev/null
@@ -0,0 +1,848 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/// <amd-dependency path='TYPO3/CMS/Core/Contrib/imagesloaded.pkgd.min' name='ImagesLoaded'>
+/// <amd-dependency path='TYPO3/CMS/Backend/Modal' name='Modal'>
+/// <amd-dependency path='TYPO3/CMS/Backend/Severity' name='Severity'>
+
+import $ = require('jquery');
+import 'jquery-ui/draggable';
+import 'jquery-ui/resizable';
+declare const Modal: any;
+declare const Severity: any;
+declare const ImagesLoaded: any;
+
+declare global {
+  interface Window {
+    TYPO3: any;
+  }
+}
+
+type Area = {
+  x: number;
+  y: number;
+  height: number;
+  width: number;
+}
+
+type Ratio = {
+  id: string;
+  title: string;
+  value: number;
+}
+
+type CropVariant = {
+  title: string;
+  id: string;
+  selectedRatio: string;
+  cropArea?: Area;
+  focusArea?: Area;
+  coverAreas?: Area[];
+  allowedAspectRatios: Ratio[];
+}
+
+type Offset = {
+  left: number;
+  top: number;
+};
+
+interface CropperEvent {
+  x: number;
+  y: number;
+  width: number;
+  height: number;
+  rotate: number;
+  scaleX: number;
+  scaleY: number;
+}
+
+interface CropperImageData {
+  left: number;
+  top: number;
+  width: number;
+  height: number;
+  naturalWidth: number;
+  naturalHeight: number;
+  aspectRatio: number;
+  rotate: number;
+  scaleX: number;
+  scaleY: number;
+}
+
+/**
+ * Module: TYPO3/CMS/Backend/ImageManipulation
+ * Contains all logic for the image crop GUI including setting focusAreas
+ * @exports TYPO3/CMS/Backend/ImageManipulation
+ */
+class ImageManipulation {
+  /**
+   * @method isCropAreaEmpty
+   * @desc Checks if an area is set or pristine
+   * @param {Area} area - The area to check
+   * @return {boolean}
+   * @static
+   */
+  public static isEmptyArea(area: Area): boolean {
+    return $.isEmptyObject(area);
+  }
+
+  /**
+   * @method wait
+   * @desc window.setTimeout shim
+   * @param {Function} fn - The function to execute
+   * @param {number} ms - The time in [ms] to wait until execution
+   * @return {boolean}
+   * @public
+   * @static
+   */
+  public static wait(fn: Function, ms: number): void {
+    window.setTimeout(fn, ms);
+  }
+
+  /**
+   * @method toCssPercent
+   * @desc Takes a number, and converts it to CSS percentage length
+   * @param {number} num - The number to convert
+   * @return {string}
+   * @public
+   * @static
+   */
+  public static toCssPercent(num: number): string {
+    return `${num * 100}%`;
+  }
+
+  /**
+   * @method serializeCropVariants
+   * @desc Serializes crop variants for persistence or preview
+   * @param {Object} cropVariants
+   * @returns string
+   */
+  private static serializeCropVariants(cropVariants: Object): string {
+    const omitUnused: any = (key: any, value: any): any =>
+      (
+        key === 'id'
+        || key === 'title'
+        || key === 'allowedAspectRatios'
+        || key === 'coverAreas'
+      ) ? undefined : value;
+
+    return JSON.stringify(cropVariants, omitUnused);
+  }
+
+  private trigger: JQuery;
+  private currentModal: JQuery;
+  private cropVariantTriggers: JQuery;
+  private activeCropVariantTrigger: JQuery;
+  private saveButton: JQuery;
+  private previewButton: JQuery;
+  private dismissButton: JQuery;
+  private resetButton: JQuery;
+  private aspectRatioTrigger: JQuery;
+  private cropperCanvas: JQuery;
+  private cropInfo: JQuery;
+  private coverAreaSelector: string = '.t3js-cropper-cover-area';
+  private cropInfoSelector: string = '.t3js-cropper-info-crop';
+  private focusAreaSelector: string = '#t3js-cropper-focus-area';
+  private focusArea: any;
+  private cropBox: JQuery;
+  private cropper: any;
+  private currentCropVariant: CropVariant;
+  private data: Object;
+  private defaultFocusArea: Area = {
+    height: 1 / 3,
+    width: 1 / 3,
+    x: 0,
+    y: 0,
+  };
+  private defaultOpts: Object = {
+    autoCrop: true,
+    autoCropArea: '0.7',
+    dragMode: 'crop',
+    guides: true,
+    responsive: true,
+    viewMode: 1,
+    zoomable: false,
+  };
+
+  constructor() {
+    // Silence is golden
+    $(window).resize((): void => {
+      if (this.cropper) {
+        this.cropper.cropper('destroy');
+      }
+    });
+    this.resizeEnd((): void => {
+      if (this.cropper) {
+        this.init();
+      }
+    });
+  }
+
+  /**
+   * @method initializeTrigger
+   * @desc Assign a handler to .t3js-image-manipulation-trigger.
+   *       Show the modal and kick-off image manipulation
+   * @public
+   */
+  public initializeTrigger(): void {
+    const triggerHandler: Function = (e: JQueryEventObject): void => {
+      e.preventDefault();
+      this.trigger = $(e.currentTarget);
+      this.show();
+    };
+    $('.t3js-image-manipulation-trigger').off('click').click(triggerHandler);
+  }
+
+  /**
+   * Initialize the cropper modal
+   */
+  private initializeCropperModal(): void {
+    const image: JQuery = this.currentModal.find('#t3js-crop-image');
+    ImagesLoaded(image, (): void => {
+      const modal: JQuery = this.currentModal.find('.modal-dialog');
+      modal.css({marginLeft: 'auto', marginRight: 'auto'});
+      modal.addClass('modal-image-manipulation modal-resize');
+      Modal.center();
+      setTimeout((): void => {
+        this.init();
+      }, 100);
+    });
+  }
+
+  private show(): void {
+    const modalTitle: string = this.trigger.data('modalTitle');
+    const imageUri: string = this.trigger.data('url');
+    const initCropperModal: Function = this.initializeCropperModal.bind(this);
+
+    /**
+     * Open modal with image to crop
+     */
+    this.currentModal = Modal.loadUrl(
+      modalTitle,
+      Severity.notice,
+      [],
+      imageUri,
+      initCropperModal,
+      '.modal-content'
+    );
+    this.currentModal.addClass('modal-dark');
+  }
+
+  private init(): void {
+    const image: JQuery = this.currentModal.find('#t3js-crop-image');
+    const imageHeight: number = $(image).height();
+    const imageWidth: number = $(image).width();
+    const data: string = this.trigger.attr('data-crop-variants');
+
+    if (!data) {
+      throw new TypeError('ImageManipulation: No cropVariants data found for image');
+    }
+
+    // If we have data already set we assume an internal reinit eg. after resizing
+    this.data = $.isEmptyObject(this.data) ? JSON.parse(data) : this.data;
+    // Initialize our class members
+    this.currentModal.find('.cropper-image-container').css({height: imageHeight, width: imageWidth});
+    this.cropVariantTriggers = this.currentModal.find('.t3js-crop-variant-trigger');
+    this.activeCropVariantTrigger = this.currentModal.find('.t3js-crop-variant-trigger.is-active');
+    this.cropInfo = this.currentModal.find(this.cropInfoSelector);
+    this.saveButton = this.currentModal.find('[data-method=save]');
+    this.previewButton = this.currentModal.find('[data-method=preview]');
+    this.dismissButton = this.currentModal.find('[data-method=dismiss]');
+    this.resetButton = this.currentModal.find('[data-method=reset]');
+    this.cropperCanvas = this.currentModal.find('#js-crop-canvas');
+    this.aspectRatioTrigger = this.currentModal.find('[data-method=setAspectRatio]');
+    this.currentCropVariant = this.data[this.activeCropVariantTrigger.attr('data-crop-variant-id')];
+
+    /**
+     * Assign EventListener to cropVariantTriggers
+     */
+    this.cropVariantTriggers.on('click', (e: JQueryEventObject): void => {
+
+      /**
+       * Is the current cropVariantTrigger is active, bail out.
+       * Bootstrap doesn't provide this functionality when collapsing the Collaps panels
+       */
+      if ($(e.currentTarget).hasClass('is-active')) {
+        e.stopPropagation();
+        e.preventDefault();
+        return;
+      }
+
+      this.activeCropVariantTrigger.removeClass('is-active');
+      $(e.currentTarget).addClass('is-active');
+      this.activeCropVariantTrigger = $(e.currentTarget);
+      let cropVariant: CropVariant = this.data[this.activeCropVariantTrigger.attr('data-crop-variant-id')];
+      const imageData: CropperImageData = this.cropper.cropper('getImageData');
+      cropVariant.cropArea = this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
+      this.currentCropVariant = $.extend(true, {}, cropVariant);
+      this.update(cropVariant);
+    });
+
+    /**
+     * Assign EventListener to aspectRatioTrigger
+     */
+    this.aspectRatioTrigger.on('click', (e: JQueryEventObject): void => {
+      const ratioId: string = $(e.currentTarget).attr('data-option');
+      const temp: CropVariant = $.extend(true, {}, this.currentCropVariant);
+      const ratio: Ratio = temp.allowedAspectRatios[ratioId];
+      this.updateAspectRatio(ratio);
+      // Set data explicitly or updateAspectRatio upscales the crop
+      this.setCropArea(temp.cropArea);
+      this.currentCropVariant = $.extend(true, {}, temp, {selectedRatio: ratioId});
+      this.update(this.currentCropVariant);
+    });
+
+    /**
+     * Assign EventListener to saveButton
+     */
+    this.saveButton.on('click', (): void => {
+      this.save(this.data);
+    });
+
+    /**
+     * Assign EventListener to previewButton if preview url exists
+     */
+    if (this.trigger.attr('data-preview-url')) {
+      this.previewButton.on('click', (): void => {
+        this.openPreview(this.data);
+      });
+    } else {
+      this.previewButton.hide();
+    }
+
+    /**
+     * Assign EventListener to dismissButton
+     */
+    this.dismissButton.on('click', (): void => {
+      this.destroy();
+    });
+
+    /**
+     * Assign EventListener to resetButton
+     */
+    this.resetButton.on('click', (e: JQueryEventObject): void => {
+      const imageData: CropperImageData = this.cropper.cropper('getImageData');
+      const resetCropVariantString: string = $(e.currentTarget).attr('data-crop-variant');
+      e.preventDefault();
+      e.stopPropagation();
+      if (!resetCropVariantString) {
+        throw new TypeError('TYPO3 Cropper: No cropVariant data attribute found on reset element.');
+      }
+      const resetCropVariant: CropVariant = JSON.parse(resetCropVariantString);
+      const absoluteCropArea: Area = this.convertRelativeToAbsoluteCropArea(resetCropVariant.cropArea, imageData);
+      this.currentCropVariant = $.extend(true, {}, resetCropVariant, {cropArea: absoluteCropArea});
+      this.update(this.currentCropVariant);
+    });
+
+    // If we start without an cropArea, maximize the cropper
+    if (ImageManipulation.isEmptyArea(this.currentCropVariant.cropArea)) {
+      this.defaultOpts = $.extend({
+        autoCropArea: 1,
+      }, this.defaultOpts);
+    }
+
+    /**
+     * Initialise the cropper
+     *
+     * Note: We use the extraneous jQuery object here, as CropperJS won't work inside the <iframe>
+     * The top.require is now inlined @see ImageManipulationElemen.php:143
+     * TODO: Find a better solution for cross iframe communications
+     */
+    this.cropper = (<any> top.TYPO3.jQuery(image)).cropper($.extend(this.defaultOpts, {
+      built: this.cropBuiltHandler,
+      crop: this.cropMoveHandler,
+      cropend: this.cropEndHandler,
+      cropstart: this.cropStartHandler,
+      data: this.currentCropVariant.cropArea,
+    }));
+  }
+
+  private cropBuiltHandler = (): void => {
+    const imageData: CropperImageData = this.cropper.cropper('getImageData');
+    this.currentCropVariant.cropArea = this.convertRelativeToAbsoluteCropArea(
+      this.currentCropVariant.cropArea,
+      imageData
+    );
+    this.cropBox = this.currentModal.find('.cropper-crop-box');
+
+    this.setCropArea(this.currentCropVariant.cropArea);
+
+    // Check if new cropVariant has coverAreas
+    if (this.currentCropVariant.coverAreas) {
+      // Init or reinit focusArea
+      this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
+    }
+    // Check if new cropVariant has focusArea
+    if (this.currentCropVariant.focusArea) {
+      // Init or reinit focusArea
+      if (ImageManipulation.isEmptyArea(this.currentCropVariant.focusArea)) {
+        // If an empty focusArea is set initialise it with the default
+        this.currentCropVariant.focusArea = $.extend(true, {}, this.defaultFocusArea);
+      }
+      this.initFocusArea(this.cropBox);
+      this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea);
+    }
+
+    if (this.currentCropVariant.selectedRatio) {
+      this.updateAspectRatio(this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio]);
+      // Set data explicitly or updateAspectRatio up-scales the crop
+      this.setCropArea(this.currentCropVariant.cropArea);
+      this.currentModal.find(`[data-option='${this.currentCropVariant.selectedRatio}']`).addClass('active');
+    }
+    this.cropperCanvas.addClass('is-visible');
+  };
+
+  private cropMoveHandler = (e: CropperEvent): void => {
+    this.currentCropVariant.cropArea = $.extend(true, this.currentCropVariant.cropArea, {
+      height: Math.floor(e.height),
+      width: Math.floor(e.width),
+      x: Math.floor(e.x),
+      y: Math.floor(e.y),
+    });
+    this.updatePreviewThumbnail(this.currentCropVariant);
+    this.updateCropVariantData(this.currentCropVariant);
+    this.cropInfo.text(`${this.currentCropVariant.cropArea.width}×${this.currentCropVariant.cropArea.height} px`);
+  };
+
+  private cropStartHandler = (): void => {
+    if (this.currentCropVariant.focusArea) {
+      this.focusArea.draggable('option', 'disabled', true);
+      this.focusArea.resizable('option', 'disabled', true);
+    }
+  };
+
+  /**
+   *
+   */
+  private cropEndHandler = (): void => {
+    if (this.currentCropVariant.focusArea) {
+      this.focusArea.draggable('option', 'disabled', false);
+      this.focusArea.resizable('option', 'disabled', false);
+    }
+  };
+
+  /**
+   * @method update
+   * @desc Update current cropArea position and size when changing cropVariants
+   * @param {CropVariant} cropVariant - The new cropVariant to update the UI with
+   */
+  private update(cropVariant: CropVariant): void {
+    const temp: CropVariant = $.extend(true, {}, cropVariant);
+    const selectedRatio: Ratio = cropVariant.allowedAspectRatios[cropVariant.selectedRatio];
+    this.currentModal.find('[data-option]').removeClass('active');
+    this.currentModal.find(`[data-option="${cropVariant.selectedRatio}"]`).addClass('active');
+    /**
+     * Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
+     */
+    this.updateAspectRatio(selectedRatio);
+    this.setCropArea(temp.cropArea);
+    this.currentCropVariant = $.extend(true, {}, temp, cropVariant);
+    this.cropBox.find(this.coverAreaSelector).remove();
+
+    // If the current container has a focus area element, deregister and cleanup prior to initialization
+    if (this.cropBox.has(this.focusAreaSelector).length) {
+      this.focusArea.resizable('destroy').draggable('destroy');
+      this.focusArea.remove();
+    }
+
+    // Check if new cropVariant has focusArea
+    if (cropVariant.focusArea) {
+      // Init or reinit focusArea
+      if (ImageManipulation.isEmptyArea(cropVariant.focusArea)) {
+        this.currentCropVariant.focusArea = $.extend(true, {}, this.defaultFocusArea);
+      }
+      this.initFocusArea(this.cropBox);
+      this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea);
+    }
+
+    // Check if new cropVariant has coverAreas
+    if (cropVariant.coverAreas) {
+      // Init or reinit focusArea
+      this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
+    }
+    this.updatePreviewThumbnail(this.currentCropVariant);
+  }
+
+  /**
+   * @method initFocusArea
+   * @desc Initializes the focus area inside a container and registers the resizable and draggable interfaces to it
+   * @param container: JQuery
+   */
+  private initFocusArea(container: JQuery): void {
+    this.focusArea = $('<div id="t3js-cropper-focus-area" class="cropper-focus-area"></div>');
+    container.append(this.focusArea);
+    this.focusArea
+      .draggable({
+        containment: container,
+        create: (): void => {
+          this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea);
+        },
+        drag: (): void => {
+          const {left, top}: Offset = container.offset();
+          const {left: fLeft, top: fTop}: Offset = this.focusArea.offset();
+          const {focusArea, coverAreas}: {focusArea?: Area, coverAreas?: Area[]} = this.currentCropVariant;
+
+          focusArea.x = (fLeft - left) / container.width();
+          focusArea.y = (fTop - top) / container.height();
+          this.updatePreviewThumbnail(this.currentCropVariant);
+          if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+            this.focusArea.addClass('has-nodrop');
+          } else {
+            this.focusArea.removeClass('has-nodrop');
+          }
+        },
+        revert: (): boolean => {
+          const revertDelay: number = 250;
+          const {left, top}: Offset = container.offset();
+          const {left: fLeft, top: fTop}: Offset = this.focusArea.offset();
+          const {focusArea, coverAreas}: {focusArea?: Area, coverAreas?: Area[]} = this.currentCropVariant;
+
+          if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+            this.focusArea.removeClass('has-nodrop');
+            ImageManipulation.wait((): void => {
+              focusArea.x = (fLeft - left) / container.width();
+              focusArea.y = (fTop - top) / container.height();
+              this.updateCropVariantData(this.currentCropVariant);
+            }, revertDelay);
+            return true;
+          }
+        },
+        revertDuration: 200,
+        stop: (): void => {
+          const {left, top}: Offset = container.offset();
+          const {left: fLeft, top: fTop}: Offset = this.focusArea.offset();
+          const {focusArea}: {focusArea?: Area} = this.currentCropVariant;
+
+          focusArea.x = (fLeft - left) / container.width();
+          focusArea.y = (fTop - top) / container.height();
+
+          this.scaleAndMoveFocusArea(focusArea);
+        },
+      })
+      .resizable({
+        containment: container,
+        handles: 'all',
+        resize: (): void => {
+          const {left, top}: Offset = container.offset();
+          const {left: fLeft, top: fTop}: Offset = this.focusArea.offset();
+          const {focusArea, coverAreas}: {focusArea?: Area, coverAreas?: Area[]} = this.currentCropVariant;
+
+          focusArea.height = this.focusArea.height() / container.height();
+          focusArea.width = this.focusArea.width() / container.width();
+          focusArea.x = (fLeft - left) / container.width();
+          focusArea.y = (fTop - top) / container.height();
+          this.updatePreviewThumbnail(this.currentCropVariant);
+
+          if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+            this.focusArea.addClass('has-nodrop');
+          } else {
+            this.focusArea.removeClass('has-nodrop');
+          }
+
+        },
+        stop: (event: any, ui: any): void => {
+          const revertDelay: number = 250;
+          const {left, top}: Offset = container.offset();
+          const {left: fLeft, top: fTop}: Offset = this.focusArea.offset();
+          const {focusArea, coverAreas}: {focusArea?: Area, coverAreas?: Area[]} = this.currentCropVariant;
+
+          if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+            ui.element.animate($.extend(ui.originalPosition, ui.originalSize), revertDelay, (): void => {
+
+              focusArea.height = this.focusArea.height() / container.height();
+              focusArea.height = this.focusArea.height() / container.height();
+              focusArea.width = this.focusArea.width() / container.width();
+              focusArea.x = (fLeft - left) / container.width();
+              focusArea.y = (fTop - top) / container.height();
+
+              this.scaleAndMoveFocusArea(focusArea);
+              this.focusArea.removeClass('has-nodrop');
+            });
+          } else {
+            this.scaleAndMoveFocusArea(focusArea);
+          }
+        },
+      });
+  }
+
+  /**
+   * @method initCoverAreas
+   * @desc Initialise cover areas inside the cropper container
+   * @param {JQuery} container - The container element to append the cover areas
+   * @param {Array<Area>} coverAreas - An array of areas to construxt the cover area elements from
+   */
+  private initCoverAreas(container: JQuery, coverAreas: Area[]): void {
+    coverAreas.forEach((coverArea: Area): void => {
+      let coverAreaCanvas: JQuery = $('<div class="t3js-cropper-cover-area"></div>');
+      container.append(coverAreaCanvas);
+      coverAreaCanvas.css({
+        height: ImageManipulation.toCssPercent(coverArea.height),
+        left: ImageManipulation.toCssPercent(coverArea.x),
+        top: ImageManipulation.toCssPercent(coverArea.y),
+        width: ImageManipulation.toCssPercent(coverArea.width),
+      });
+    });
+  }
+
+  /**
+   * @method updatePreviewThumbnail
+   * @desc Sync the croping (and focus area) to the preview thumbnail
+   * @param {CropVariant} cropVariant
+   */
+  private updatePreviewThumbnail(cropVariant: CropVariant): void {
+    let styles: any;
+    const cropperPreviewThumbnailCrop: JQuery =
+      this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
+    const cropperPreviewThumbnailImage: JQuery =
+      this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
+    const cropperPreviewThumbnailFocus: JQuery =
+      this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
+    const imageData: CropperImageData = this.cropper.cropper('getImageData');
+
+    // Update the position/dimension of the crop area in the preview
+    cropperPreviewThumbnailCrop.css({
+      height: ImageManipulation.toCssPercent(cropVariant.cropArea.height / imageData.naturalHeight),
+      left: ImageManipulation.toCssPercent(cropVariant.cropArea.x / imageData.naturalWidth),
+      top: ImageManipulation.toCssPercent(cropVariant.cropArea.y / imageData.naturalHeight),
+      width: ImageManipulation.toCssPercent(cropVariant.cropArea.width / imageData.naturalWidth),
+    });
+
+    // Show and update focusArea in the preview only if we really have one configured
+    if (cropVariant.focusArea) {
+      cropperPreviewThumbnailFocus.css({
+        height: ImageManipulation.toCssPercent(cropVariant.focusArea.height),
+        left: ImageManipulation.toCssPercent(cropVariant.focusArea.x),
+        top: ImageManipulation.toCssPercent(cropVariant.focusArea.y),
+        width: ImageManipulation.toCssPercent(cropVariant.focusArea.width),
+      });
+    }
+
+    // Destruct the preview container's CSS properties
+    styles = cropperPreviewThumbnailCrop.css([
+      'width', 'height', 'left', 'top',
+    ]);
+
+    /**
+     * Apply negative margins on the previewThumbnailImage to make the illusion of an offset
+     */
+    cropperPreviewThumbnailImage.css({
+      height: `${parseFloat(styles.height) * (1 / (cropVariant.cropArea.height / imageData.naturalHeight))}px`,
+      margin: `${-1 * parseFloat(styles.left)}px`,
+      marginTop: `${-1 * parseFloat(styles.top)}px`,
+      width: `${parseFloat(styles.width) * (1 / (cropVariant.cropArea.width / imageData.naturalWidth))}px`,
+    });
+  }
+
+  /**
+   * @method scaleAndMoveFocusArea
+   * @desc Calculation logic for moving the focus area given the
+   *       specified constrains of a crop and an optional cover area
+   * @param {Area} focusArea - The translation data
+   */
+  private scaleAndMoveFocusArea(focusArea: Area): void {
+    this.focusArea.css({
+      height: ImageManipulation.toCssPercent(focusArea.height),
+      left: ImageManipulation.toCssPercent(focusArea.x),
+      top: ImageManipulation.toCssPercent(focusArea.y),
+      width: ImageManipulation.toCssPercent(focusArea.width),
+    });
+    this.currentCropVariant.focusArea = focusArea;
+    this.updatePreviewThumbnail(this.currentCropVariant);
+    this.updateCropVariantData(this.currentCropVariant);
+  }
+
+  /**
+   * @method updateCropVariantData
+   * @desc Immutably updates the currently selected cropVariant data
+   * @param {CropVariant} currentCropVariant - The cropVariant to immutably save
+   */
+  private updateCropVariantData(currentCropVariant: CropVariant): void {
+    const imageData: CropperImageData = this.cropper.cropper('getImageData');
+    const absoluteCropArea: Area = this.convertAbsoluteToRelativeCropArea(currentCropVariant.cropArea, imageData);
+    this.data[currentCropVariant.id] = $.extend(true, {}, currentCropVariant, {cropArea: absoluteCropArea});
+  }
+
+  /**
+   * @method updateAspectRatio
+   * @desc Updates the aspect ratio in the cropper
+   * @param {ratio} ratio ratio set in the cropper
+   */
+  private updateAspectRatio(ratio: Ratio): void {
+    this.cropper.cropper('setAspectRatio', ratio.value);
+  }
+
+  /**
+   * @method setCropArea
+   * @desc Updates the crop area in the cropper. The cropper will respect the selected ratio
+   * @param {cropArea} cropArea ratio set in the cropper
+   */
+  private setCropArea(cropArea: Area): void {
+    this.cropper.cropper('setData', {
+      height: cropArea.height,
+      width: cropArea.width,
+      x: cropArea.x,
+      y: cropArea.y,
+    });
+  }
+
+  /**
+   * @method checkFocusAndCoverAreas
+   * @desc Checks is one focus area and one or more cover areas overlap
+   * @param focusArea
+   * @param coverAreas
+   * @return {boolean}
+   */
+  private checkFocusAndCoverAreasCollision(focusArea: Area, coverAreas: Area[]): boolean {
+    return coverAreas
+      .some((coverArea: Area): boolean => {
+        // noinspection OverlyComplexBooleanExpressionJS
+        if (focusArea.x < coverArea.x + coverArea.width &&
+          focusArea.x + focusArea.width > coverArea.x &&
+          focusArea.y < coverArea.y + coverArea.height &&
+          focusArea.height + focusArea.y > coverArea.y) {
+          return true;
+        }
+      });
+  }
+
+  /**
+   * @param cropArea
+   * @param imageData
+   * @return {{height: number, width: number, x: number, y: number}}
+   */
+  private convertAbsoluteToRelativeCropArea(cropArea: Area, imageData: CropperImageData): Area {
+    const {height, width, x, y}: Area = cropArea;
+    return {
+      height: height / imageData.naturalHeight,
+      width: width / imageData.naturalWidth,
+      x: x / imageData.naturalWidth,
+      y: y / imageData.naturalHeight,
+    };
+  }
+
+  /**
+   * @param cropArea
+   * @param imageData
+   * @return {{height: number, width: number, x: number, y: number}}
+   */
+  private convertRelativeToAbsoluteCropArea(cropArea: Area, imageData: CropperImageData): Area {
+    const {height, width, x, y}: Area = cropArea;
+    return {
+      height: height * imageData.naturalHeight,
+      width: width * imageData.naturalWidth,
+      x: x * imageData.naturalWidth,
+      y: y * imageData.naturalHeight,
+    };
+  }
+
+  private setPreviewImage(data: Object): void {
+    let $image: any = this.cropper;
+    let imageData: CropperImageData = $image.cropper('getImageData');
+    Object.keys(data).forEach((cropVariantId: string) => {
+      const cropVariant: CropVariant = data[cropVariantId];
+      const cropData: Area = this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
+
+      let $preview: JQuery = this.trigger
+        .closest('.form-group')
+        .find(`.t3js-image-manipulation-preview[data-crop-variant-id="${cropVariantId}"]`);
+
+      if ($preview.length === 0) {
+        return;
+      }
+
+      let previewWidth: number = $preview.data('preview-width');
+      let previewHeight: number = $preview.data('preview-height');
+
+      // Adjust aspect ratio of preview width/height
+      let aspectRatio: number = cropData.width / cropData.height;
+      let tmpHeight: number = previewWidth / aspectRatio;
+      if (tmpHeight > previewHeight) {
+        previewWidth = previewHeight * aspectRatio;
+      } else {
+        previewHeight = tmpHeight;
+      }
+      // preview should never be up-scaled
+      if (previewWidth > cropData.width) {
+        previewWidth = cropData.width;
+        previewHeight = cropData.height;
+      }
+
+      let ratio: number = previewWidth / cropData.width;
+
+      let $viewBox: JQuery = $('<div />').html('<img src="' + $image.attr('src') + '">');
+      $viewBox.addClass('cropper-preview-container');
+      $preview.empty().append($viewBox);
+      $viewBox.wrap('<span class="thumbnail thumbnail-status"></span>');
+
+      $viewBox.width(previewWidth).height(previewHeight).find('img').css({
+        height: imageData.naturalHeight * ratio,
+        left: -cropData.x * ratio,
+        top: -cropData.y * ratio,
+        width: imageData.naturalWidth * ratio,
+      });
+    });
+  };
+
+  /**
+   * @method openPreview
+   * @desc open a preview
+   * @param {object} data - The whole data object containing all the cropVariants
+   * @private
+   */
+  private openPreview(data: Object): void {
+    const cropVariants: string = ImageManipulation.serializeCropVariants(data);
+    let previewUrl: string = this.trigger.attr('data-preview-url');
+    previewUrl = previewUrl + '&cropVariants=' + encodeURIComponent(cropVariants);
+    window.open(previewUrl, 'TYPO3ImageManipulationPreview');
+  }
+
+  /**
+   * @method save
+   * @desc Saves the edited cropVariants to a hidden field
+   * @param {object} data - The whole data object containing all the cropVariants
+   * @private
+   */
+  private save(data: Object): void {
+    const cropVariants: string = ImageManipulation.serializeCropVariants(data);
+    const hiddenField: JQuery = $(`#${this.trigger.attr('data-field')}`);
+    this.trigger.attr('data-crop-variants', JSON.stringify(data));
+    this.setPreviewImage(data);
+    hiddenField.val(cropVariants);
+    this.destroy();
+  }
+
+  /**
+   * @method destroy
+   * @desc Destroy the ImageManipulation including cropper and alike
+   * @private
+   */
+  private destroy(): void {
+    if (this.currentModal) {
+      this.currentModal.modal('hide');
+      this.cropper.cropper('destroy');
+      this.currentModal = null;
+    }
+  }
+
+  private resizeEnd(fn: Function): void {
+    let timer: number;
+    $(window).on('resize', (): void => {
+      clearTimeout(timer);
+      timer = setTimeout((): void => {
+        fn();
+      }, 450);
+    });
+  }
+}
+
+export = new ImageManipulation();
index 7aee3a2..a69aab5 100644 (file)
@@ -320,8 +320,7 @@ th {
 :root .fa-rotate-270,
 :root .fa-flip-horizontal,
 :root .fa-flip-vertical {
-  -webkit-filter: none;
-          filter: none;
+  filter: none;
 }
 .fa-stack {
   position: relative;
@@ -8903,13 +8902,14 @@ div.dropdown-menu {
 }
 /*!
  * Datetimepicker for Bootstrap 3
- * version : 4.17.42
+ * version : 4.17.45
  * https://github.com/Eonasdan/bootstrap-datetimepicker/
  */
 .bootstrap-datetimepicker-widget {
   list-style: none;
 }
 .bootstrap-datetimepicker-widget.dropdown-menu {
+  display: block;
   margin: 2px 0;
   padding: 4px;
   width: 19em;
@@ -9655,22 +9655,381 @@ iframe,
 .collapse.in {
   height: auto;
 }
-.cropper-line {
-  background-color: #FFFFFF;
+.cropper {
+  /**
+        * Style overrides for CropperJS CSS and additional styles to make cropper look like expected
+        *
+        * 1. Fixes hidden drag/resize handles for cropper
+        * 2. Fixes containment on the jQuery UI resizeable
+        * 3. Offset cropper lines and handles to inside of container
+        */
 }
-.cropper-point {
-  background-color: #FFFFFF;
+.cropper .cropper-line {
+  background-color: transparent;
 }
-.cropper-point.point-se:before {
-  background-color: #FFFFFF;
+.cropper .cropper-dashed {
+  border: 1px dashed #ff8700;
 }
-.cropper-view-box {
-  outline: 1px dashed #6699ff;
-  outline-color: #ffffff;
+.cropper .cropper-point {
+  background-color: #ff8700;
 }
-.cropper-bg {
+.cropper .cropper-point.point-nw {
+  left: 0;
+  /* 3 */
+  top: 0;
+  /* 3 */
+}
+.cropper .cropper-point.point-w {
+  left: 0;
+  /* 3 */
+}
+.cropper .cropper-point.point-sw {
+  left: 0;
+  /* 3 */
+  bottom: 0;
+  /* 3 */
+}
+.cropper .cropper-point.point-ne {
+  right: 0;
+  /* 3 */
+  top: 0;
+  /* 3 */
+}
+.cropper .cropper-point.point-e {
+  right: 0;
+  /* 3 */
+}
+.cropper .cropper-point.point-se {
+  right: 0;
+  /* 3 */
+  bottom: 0;
+  /* 3 */
+}
+.cropper .cropper-point.point-se:before {
+  background-color: #fff;
+}
+.cropper .cropper-point.point-n {
+  top: 0;
+  /* 3 */
+}
+.cropper .cropper-point.point-s {
+  bottom: 0;
+  /* 3 */
+}
+.cropper .cropper-view-box {
+  outline: 1px dashed #ff8700;
+}
+.cropper .cropper-bg {
   background-image: url("../Images/cropper-background.png");
 }
+.cropper .cropper-image-container {
+  height: 100% !important;
+  width: 100% !important;
+  max-width: 1000px !important;
+}
+.cropper .ratio-buttons .btn.btn-default {
+  margin-bottom: 5px;
+}
+.cropper .panel-group {
+  position: relative;
+  margin: -15px;
+}
+.cropper .panel-group [aria-expanded=true] {
+  border-left: 2px solid #ff8700;
+  position: relative;
+}
+.cropper .panel-group [aria-expanded=true]:before {
+  position: absolute;
+  left: -10px;
+  top: 50%;
+  margin-top: -10px;
+  content: " ";
+  width: 0;
+  height: 0;
+  border-style: solid;
+  border-width: 10px 10px 10px 0;
+  border-color: transparent #ff8700 transparent transparent;
+}
+.cropper .panel-group [aria-expanded=true][data-toggle=collapse] {
+  background-color: #333;
+}
+.cropper .panel-group [aria-expanded=false] {
+  border-left: 2px solid #444;
+  position: relative;
+}
+.cropper .panel-group table,
+.cropper .panel-group label {
+  color: #fff;
+}
+.cropper .panel-collapse.collapse {
+  background-color: #2c2c2c !important;
+  border-left: 2px solid #ff8700;
+}
+.cropper .panel-heading {
+  padding: 0;
+}
+.cropper .panel-heading .panel-title > [data-crop-variant] {
+  display: -ms-flexbox;
+  display: flex;
+  padding: 10px 15px;
+  -ms-flex-pack: justify;
+      justify-content: space-between;
+}
+.cropper .panel-heading .panel-title > a {
+  text-decoration: none !important;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+  font-size: 13px;
+}
+.cropper .panel-default > .panel-heading + .panel-collapse > .panel-body {
+  border-top-color: #333;
+}
+.cropper .panel-group,
+.cropper .panel-default .panel-heading,
+.cropper .panel {
+  background: #222222;
+  border: none;
+  color: #fff;
+}
+.cropper .btn-secondary,
+.cropper .open .dropdown-toggle.btn-secondary {
+  color: #fff;
+  background-color: #777;
+  border-color: #555;
+}
+.cropper .btn-secondary:hover,
+.cropper .btn-secondary:focus,
+.cropper .btn-secondary:active,
+.cropper .btn-secondary.active {
+  color: #fff;
+  background-color: #888;
+  border-color: #444;
+}
+.cropper .cropper-container.cropper-bg {
+  overflow: visible;
+  /* 1 */
+}
+.cropper .cropper-crop-box {
+  overflow: hidden;
+  /* 2 */
+}
+.cropper .cropper-crop-box:after {
+  background-color: #ff8700;
+  content: "Cropped area";
+  position: absolute;
+  left: 0;
+  top: 0;
+  font-size: 10px;
+  color: black;
+  height: 16px;
+  width: 100%;
+  max-width: 80px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  padding: 0 4px;
+  pointer-events: none;
+  overflow: hidden;
+}
+.cropper .cropper-line.line-w {
+  left: 0;
+  /* 3 */
+}
+.cropper .cropper-line.line-e {
+  right: 0;
+  /* 3 */
+}
+.cropper .cropper-line.line-n {
+  top: 0;
+  /* 3 */
+}
+.cropper .cropper-line.line-s {
+  bottom: 0;
+  /* 3 */
+}
+.cropper .ui-resizable-handle.ui-resizable-n,
+.cropper .ui-resizable-handle.ui-resizable-s,
+.cropper .ui-resizable-handle.ui-resizable-e,
+.cropper .ui-resizable-handle.ui-resizable-w {
+  border-color: transparent;
+  transform: none;
+}
+.cropper .ui-resizable-handle.ui-resizable-e,
+.cropper .ui-resizable-handle.ui-resizable-w {
+  width: 6px;
+}
+.cropper .ui-resizable-handle.ui-resizable-n,
+.cropper .ui-resizable-handle.ui-resizable-s {
+  height: 6px;
+}
+.cropper .ui-resizable-handle.ui-resizable-e {
+  right: 0;
+}
+.cropper .ui-resizable-handle.ui-resizable-w {
+  left: 0;
+}
+.cropper .ui-resizable-handle.ui-resizable-n {
+  top: 0;
+}
+.cropper .ui-resizable-handle.ui-resizable-s {
+  bottom: 0;
+}
+.cropper .ui-resizable-handle.ui-resizable-sw,
+.cropper .ui-resizable-handle.ui-resizable-se,
+.cropper .ui-resizable-handle.ui-resizable-ne,
+.cropper .ui-resizable-handle.ui-resizable-nw {
+  transform: none;
+  background-color: #ccc;
+  height: 6px;
+  width: 6px;
+}
+.cropper .ui-resizable-handle.ui-resizable-nw {
+  top: 0;
+  left: 0;
+}
+.cropper .ui-resizable-handle.ui-resizable-ne {
+  top: 0;
+  right: 0;
+}
+.cropper .ui-resizable-handle.ui-resizable-se {
+  bottom: 0;
+  right: 0;
+}
+.cropper .ui-resizable-handle.ui-resizable-sw {
+  bottom: 0;
+  left: 0;
+}
+.cropper .t3js-ratio-buttons {
+  margin-bottom: 10px;
+}
+.cropper .t3js-ratio-buttons .btn:not(.active) .fa {
+  display: none;
+}
+.cropper .cropper-focus-area {
+  cursor: move;
+  height: 200px;
+  width: 200px;
+  background-color: rgba(215, 187, 0, 0.5);
+  position: absolute;
+  z-index: 999999;
+  opacity: 1;
+  overflow: hidden;
+  transition: background-color 300ms;
+}
+.cropper .cropper-focus-area.has-nodrop,
+.cropper .cropper-focus-area.has-nodrop:hover {
+  background-color: rgba(211, 35, 46, 0.6) !important;
+  transition: background-color 300ms;
+}
+.cropper .cropper-focus-area:hover,
+.cropper .cropper-focus-area:focus {
+  background-color: rgba(215, 187, 0, 0.7);
+}
+.cropper .cropper-focus-area:after {
+  background-color: rgba(255, 255, 255, 0.95);
+  content: "Focus";
+  position: absolute;
+  left: 0;
+  top: 0;
+  font-size: 10px;
+  color: black;
+  height: 16px;
+  width: 100%;
+  max-width: 44px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  padding: 0 4px 0 8px;
+  /* Additional 4px on left due to resize handle on focus area */
+  pointer-events: none;
+  overflow: hidden;
+}
+.cropper .t3js-cropper-cover-area {
+  background: url("../Images/cropper-background-cover-area.svg");
+  pointer-events: none;
+  cursor: not-allowed;
+  position: absolute;
+  opacity: 1;
+  z-index: 99999;
+}
+.cropper .t3js-cropper-cover-area:after {
+  background-color: rgba(255, 255, 255, 0.95);
+  content: "Cover area";
+  position: absolute;
+  left: 0;
+  top: 0;
+  font-size: 10px;
+  color: black;
+  height: 16px;
+  width: 100%;
+  max-width: 80px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  padding: 0 4px;
+  pointer-events: none;
+  overflow: hidden;
+}
+.cropper .cropper-preview-thumbnail {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  position: relative;
+  max-width: 100px;
+  max-height: 100px;
+  overflow: hidden;
+}
+.cropper .cropper-preview-thumbnail:after {
+  background-color: rgba(0, 0, 0, 0.5);
+  content: " ";
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  position: absolute;
+  z-index: 9;
+}
+.cropper .cropper-preview-thumbnail.wide {
+  width: 100px;
+  height: auto;
+}
+.cropper .cropper-preview-thumbnail.tall {
+  width: auto;
+  height: 80px;
+}
+.cropper .cropper-preview-thumbnail-image {
+  left: 0;
+  top: 0;
+}
+.cropper .wide .cropper-preview-thumbnail-image {
+  width: 100%;
+  height: auto;
+}
+.cropper .tall .cropper-preview-thumbnail-image {
+  width: auto;
+  height: 100%;
+}
+.cropper .cropper-preview-thumbnail-crop-area {
+  border: 1px solid #ff8700;
+  position: absolute;
+  z-index: 10;
+  overflow: hidden;
+}
+.cropper .cropper-preview-thumbnail-focus-area {
+  background-color: rgba(215, 187, 0, 0.7);
+  position: absolute;
+  z-index: 11;
+}
+:root .cropper-preview-thumbnail-crop-image {
+  image-orientation: 0deg;
+  display: block;
+  height: 100%;
+  width: 100%;
+  min-width: 0;
+  max-width: none;
+  min-height: 0;
+  max-height: none;
+}
 .cropper-preview-container {
   overflow: hidden;
   position: relative;
@@ -9679,13 +10038,10 @@ iframe,
   display: block;
   position: absolute;
   width: 100%;
-  min-width: 0!important;
-  min-height: 0!important;
-  max-width: none!important;
-  max-height: none!important;
-}
-.ratio-buttons .btn.btn-default {
-  margin-bottom: 5px;
+  min-width: 0 !important;
+  min-height: 0 !important;
+  max-width: none !important;
+  max-height: none !important;
 }
 .typo3-csh-inline {
   padding: 4px;
@@ -10135,6 +10491,48 @@ div#contentMenu1 {
   transition-duration: 0.35s;
   transition-timing-function: ease;
 }
+/**
+ * Cropper modal component styles
+ */
+.modal-image-manipulation {
+  width: 90vw;
+  height: 90vh;
+}
+.modal-image-manipulation .modal-panel {
+  display: -ms-flexbox;
+  display: flex;
+  max-height: 80vh;
+  overflow: visible;
+}
+.modal-image-manipulation .modal-panel-body {
+  background-image: url("../Images/cropper-background.png");
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-align: center;
+      align-items: center;
+  -ms-flex-pack: center;
+      justify-content: center;
+}
+.modal-image-manipulation .modal-panel .modal-panel-body {
+  max-height: 100%;
+  width: 100% !important;
+}
+.modal-image-manipulation .modal-panel .modal-panel-sidebar {
+  min-width: 300px;
+  max-height: 100%;
+  overflow: auto;
+  -webkit-overflow-scrolling: touch;
+}
+.modal-image-manipulation .modal-panel .modal-panel-sidebar-right {
+  background-color: #212424;
+  position: relative;
+}
+.modal-image-manipulation .modal-panel .modal-footer {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  width: 100%;
+}
 .modal-image-manipulation .modal-body .col-lg-12 {
   padding-right: 450px;
 }
@@ -10163,7 +10561,7 @@ div#contentMenu1 {
   color: #FFF;
 }
 .modal.modal-dark .modal-footer {
-  border-top: none;
+  border-top: 1px solid #000000;
 }
 .modal-panel .modal-panel-body {
   float: left;
diff --git a/typo3/sysext/backend/Resources/Public/Images/cropper-background-cover-area.svg b/typo3/sysext/backend/Resources/Public/Images/cropper-background-cover-area.svg
new file mode 100644 (file)
index 0000000..4f1006f
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Created by Martin Engel  -->
+<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'>
+<rect width='10' height='10' fill='#fff' opacity='0.5'/>
+<path d='M0,10L10,0L0,10z M10.9,9.1l-2,2L10.9,9.1z M-1,1l2-2L-1,1z' stroke='#000' stroke-width='2'/>
+</svg>
index a5e1846..15a5713 100644 (file)
  *
  * The TYPO3 project - inspiring people to share!
  */
-
-/**
- * Module: TYPO3/CMS/Backend/ImageManipulation
- * Contains all logic for the image crop GUI
- */
-define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], function ($, Modal, Severity) {
-
-       /**
-        *
-        * @type {{margin: number, currentModal: null, cropperSelector: string, $trigger: null}}
-        * @exports TYPO3/CMS/Backend/ImageManipulation
-        */
-       var ImageManipulation = {
-               margin: 20,
-               currentModal: null,
-               cropperSelector: '.t3js-cropper-image-container > img',
-               $trigger: null
-       };
-
-       /**
-        * Initialize triggers
-        */
-       ImageManipulation.initializeTrigger = function() {
-               var $triggers = $('.t3js-image-manipulation-trigger');
-               // Remove existing bind function
-               $triggers.off('click', ImageManipulation.buttonClick);
-               // Bind new function
-               $triggers.on('click', ImageManipulation.buttonClick);
-       };
-
-       /**
-        * Functions that should be bind to the trigger button
-        *
-        * @param {Event} e click event
-        */
-       ImageManipulation.buttonClick = function(e) {
-               e.preventDefault();
-               // Prevent double trigger
-               if (ImageManipulation.$trigger !== $(this)) {
-                       ImageManipulation.$trigger = $(this);
-                       ImageManipulation.show();
-               }
-       };
-
-       /**
-        * Open modal with image to crop
-        */
-       ImageManipulation.show = function() {
-               ImageManipulation.currentModal = Modal.loadUrl(
-                       ImageManipulation.$trigger.data('image-name'),
-                       Severity.notice,
-                       [],
-                       ImageManipulation.$trigger.data('url'),
-                       ImageManipulation.initializeCropperModal,
-                       '.modal-content'
-               );
-               ImageManipulation.currentModal.addClass('modal-dark');
-       };
-
-       /**
-        * Initialize the cropper modal
-        */
-       ImageManipulation.initializeCropperModal = function() {
-               top.require(['cropper', 'imagesloaded'], function(cropperJs, imagesLoaded) {
-                       var $image = ImageManipulation.getCropper();
-
-                       // wait until image is loaded
-                       imagesLoaded($image, function() {
-                               var $modal = ImageManipulation.currentModal.find('.modal-dialog');
-                               var $modalContent = $modal.find('.modal-content');
-                               var $modalPanelSidebar = $modal.find('.modal-panel-sidebar');
-                               var $modalPanelBody = $modal.find('.modal-panel-body');
-                               // Let modal auto-fill width
-                               $modal.css({width:'auto', marginLeft: ImageManipulation.margin, marginRight: ImageManipulation.margin})
-                                         .addClass('modal-image-manipulation modal-resize');
-
-                               $modalContent.addClass('cropper-bg');
-
-                               // Determine available height
-                               var height = $(window).height()
-                                               - (ImageManipulation.margin * 4);
-                               $image.css({maxHeight: height});
-
-                               // Wait a few microseconds before calculating available width (DOM isn't always updated direct)
-                               setTimeout(function() {
-                                       $modalPanelBody.css({width: $modalContent.innerWidth() - $modalPanelSidebar.outerWidth() - (ImageManipulation.margin * 2)});
-
-                                       setTimeout(function() {
-                                               // Shrink modal when possible (the set left/right margin + width auto above makes it fill 100%)
-                                               var minWidth = Math.max(500, $image.outerWidth() + $modalPanelSidebar.outerWidth() + (ImageManipulation.margin * 2));
-                                               var width = $modal.width() > minWidth ? minWidth : $modal.width();
-                                               $modal.width(width);
-                                               $modalPanelBody.width(width - $modalPanelSidebar.outerWidth() - (ImageManipulation.margin * 4));
-
-                                               var modalBodyMinHeight = $modalContent.height() -
-                                                       ($modalPanelSidebar.find('.modal-header').outerHeight() + $modalPanelSidebar.find('.modal-body-footer').outerHeight());
-                                               $modalPanelSidebar.find('.modal-body').css('min-height', modalBodyMinHeight);
-
-                                               // Center modal horizontal
-                                               $modal.css({marginLeft: 'auto', marginRight: 'auto'});
-
-                                               // Center modal vertical
-                                               Modal.center();
-
-                                               // Wait a few microseconds to let the modal resize
-                                               setTimeout(ImageManipulation.initializeCropper, 100);
-                                       }, 100);
-
-                               }, 100);
-                       });
-
-               });
-       };
-
-       /**
-        * Initialize cropper
-        */
-       ImageManipulation.initializeCropper = function() {
-               var $image = ImageManipulation.getCropper(), cropData;
-
-               // Give img-container same dimensions as the image
-               ImageManipulation.currentModal.find('.t3js-cropper-image-container').
-               css({width: $image.width(), height: $image.height()});
-
-               var $trigger = ImageManipulation.$trigger;
-               var jsonString = $trigger.parent().find('#' + $trigger.data('field')).val();
-               if (jsonString.length) {
-                       cropData = JSON.parse(jsonString);
-               }
-
-               var $infoX = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-x');
-               var $infoY = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-y');
-               var $infoWidth = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-width');
-               var $infoHeight = ImageManipulation.currentModal.find('.t3js-image-manipulation-info-crop-height');
-
-               $image.cropper({
-                       autoCropArea: 0.5,
-                       strict: false,
-                       zoomable: ImageManipulation.currentModal.find('.t3js-setting-zoom').length > 0,
-                       built: function() {
-                               if (cropData) {
-                                       // Dimensions CropBox need to be the real visible dimensions
-                                       var ratio = $image.cropper('getImageData').width / $image.data('original-width');
-                                       var cropBox = {};
-                                       cropBox.left = cropData.x * ratio;
-                                       cropBox.top = cropData.y * ratio;
-                                       cropBox.width = cropData.width * ratio;
-                                       cropBox.height = cropData.height * ratio;
-                                       $image.cropper('setCropBoxData', cropBox);
-                               }
-                       },
-                       crop: function (data) {
-                               var ratio = $image.cropper('getImageData').naturalWidth / $image.data('original-width');
-                               $infoX.text(Math.round(data.x / ratio) + 'px');
-                               $infoY.text(Math.round(data.y / ratio) + 'px');
-                               $infoWidth.text(Math.round(data.width / ratio) + 'px');
-                               $infoHeight.text(Math.round(data.height / ratio) + 'px');
-                       }
-               });
-
-               // Destroy cropper when modal is closed
-               ImageManipulation.currentModal.on('hidden.bs.modal', function() {
-                       $image.cropper('destroy');
-               });
-
-               ImageManipulation.initializeCroppingActions();
-       };
-
-       /**
-        * Get image to be cropped
-        *
-        * @returns {Object} jQuery object
-        */
-       ImageManipulation.getCropper = function() {
-               return ImageManipulation.currentModal.find(ImageManipulation.cropperSelector);
-       };
-
-       /**
-        * Bind buttons from cropper tool panel
-        */
-       ImageManipulation.initializeCroppingActions = function() {
-               ImageManipulation.currentModal.find('[data-method]').click(function(e) {
-                       e.preventDefault();
-                       var method = $(this).data('method');
-                       var options = $(this).data('option') || {};
-                       if (typeof ImageManipulation[method] === 'function') {
-                               ImageManipulation[method](options);
-                       }
-               });
-       };
-
-       /**
-        * Change the aspect ratio of the crop box
-        *
-        * @param {Number} aspectRatio
-        */
-       ImageManipulation.setAspectRatio = function(aspectRatio) {
-               var $cropper = ImageManipulation.getCropper();
-               $cropper.cropper('setAspectRatio', aspectRatio);
-       };
-
-       /**
-        * Set zoom ratio
-        *
-        * Zoom in: requires a positive number (ratio > 0)
-        * Zoom out: requires a negative number (ratio < 0)
-        *
-        * @param {Number} ratio
-        */
-       ImageManipulation.zoom = function(ratio) {
-               var $cropper = ImageManipulation.getCropper();
-               $cropper.cropper('zoom', ratio);
-       };
-
-       /**
-        * Save crop values in form and close modal
-        */
-       ImageManipulation.save = function() {
-               var $image = ImageManipulation.getCropper();
-               var $trigger = ImageManipulation.$trigger;
-               var formFieldId = $trigger.data('field');
-               var $formField = $trigger.parent().find('#' + formFieldId);
-               var $formGroup = $formField.closest('.form-group');
-               var cropData = $image.cropper('getData');
-               var newValue = '';
-               $formGroup.addClass('has-change');
-               if (cropData.width > 0 && cropData.height > 0) {
-                       var ratio = $image.cropper('getImageData').naturalWidth / $image.data('original-width');
-                       cropData.x = cropData.x / ratio;
-                       cropData.y = cropData.y / ratio;
-                       cropData.width = cropData.width / ratio;
-                       cropData.height = cropData.height / ratio;
-                       newValue = JSON.stringify(cropData);
-                       $formGroup.find('.t3js-image-manipulation-info').removeClass('hide');
-                       $formGroup.find('.t3js-image-manipulation-info-crop-x').text(Math.round(cropData.x) + 'px');
-                       $formGroup.find('.t3js-image-manipulation-info-crop-y').text(Math.round(cropData.y) + 'px');
-                       $formGroup.find('.t3js-image-manipulation-info-crop-width').text(Math.round(cropData.width) + 'px');
-                       $formGroup.find('.t3js-image-manipulation-info-crop-height').text(Math.round(cropData.height) + 'px');
-                       $formGroup.find('.t3js-image-manipulation-preview').removeClass('hide');
-                       ImageManipulation.setPreviewImage();
-               } else {
-                       $formGroup.find('.t3js-image-manipulation-info').addClass('hide');
-                       $formGroup.find('.t3js-image-manipulation-preview').addClass('hide');
-               }
-               $formField.val(newValue);
-               $formField.trigger('change');
-               ImageManipulation.dismiss();
-       };
-
-       /**
-        * Reset crop selection
-        */
-       ImageManipulation.reset = function() {
-               var $image = ImageManipulation.getCropper();
-               $image.cropper('clear');
-       };
-
-       /**
-        * Close the current open modal
-        */
-       ImageManipulation.dismiss = function() {
-               if (ImageManipulation.currentModal) {
-                       ImageManipulation.currentModal.modal('hide');
-                       ImageManipulation.currentModal = null;
-               }
-       };
-
-       /**
-        * Set preview image
-        */
-       ImageManipulation.setPreviewImage = function() {
-               var $preview = ImageManipulation.$trigger.closest('.form-group').find('.t3js-image-manipulation-preview');
-               if ($preview.length === 0) {
-                       return;
-               }
-               var $image = ImageManipulation.getCropper();
-               var imageData = $image.cropper('getImageData');
-               var cropData = $image.cropper('getData');
-               var previewWidth = $preview.data('preview-width');
-               var previewHeight = $preview.data('preview-height');
-
-               // Adjust aspect ratio of preview width/height
-               var aspectRatio = cropData.width / cropData.height;
-               var tmpHeight = previewWidth / aspectRatio;
-               if (tmpHeight > previewHeight) {
-                       previewWidth = previewHeight * aspectRatio;
-               } else {
-                       previewHeight = tmpHeight;
-               }
-               // preview should never be up-scaled
-               if (previewWidth > cropData.width) {
-                       previewWidth = cropData.width;
-                       previewHeight = cropData.height;
-               }
-
-               var ratio = previewWidth / cropData.width;
-
-               var $viewBox = $('<div />').html('<img src="' + $image.attr('src') + '">');
-               $viewBox.addClass('cropper-preview-container');
-               $preview.empty().append($viewBox);
-               $viewBox.wrap('<span class="thumbnail thumbnail-status"></span>');
-
-               $viewBox.width(previewWidth).height(previewHeight).find('img').css({
-                       width: imageData.naturalWidth * ratio,
-                       height: imageData.naturalHeight * ratio,
-                       left: -cropData.x * ratio,
-                       top: -cropData.y * ratio
-               });
-       };
-
-       return ImageManipulation;
+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, $) {
+    "use strict";
+    /**
+     * Module: TYPO3/CMS/Backend/ImageManipulation
+     * Contains all logic for the image crop GUI including setting focusAreas
+     * @exports TYPO3/CMS/Backend/ImageManipulation
+     */
+    var ImageManipulation = (function () {
+        function ImageManipulation() {
+            var _this = this;
+            this.coverAreaSelector = '.t3js-cropper-cover-area';
+            this.cropInfoSelector = '.t3js-cropper-info-crop';
+            this.focusAreaSelector = '#t3js-cropper-focus-area';
+            this.defaultFocusArea = {
+                height: 1 / 3,
+                width: 1 / 3,
+                x: 0,
+                y: 0,
+            };
+            this.defaultOpts = {
+                autoCrop: true,
+                autoCropArea: '0.7',
+                dragMode: 'crop',
+                guides: true,
+                responsive: true,
+                viewMode: 1,
+                zoomable: false,
+            };
+            this.cropBuiltHandler = function () {
+                var imageData = _this.cropper.cropper('getImageData');
+                _this.currentCropVariant.cropArea = _this.convertRelativeToAbsoluteCropArea(_this.currentCropVariant.cropArea, imageData);
+                _this.cropBox = _this.currentModal.find('.cropper-crop-box');
+                _this.setCropArea(_this.currentCropVariant.cropArea);
+                // Check if new cropVariant has coverAreas
+                if (_this.currentCropVariant.coverAreas) {
+                    // Init or reinit focusArea
+                    _this.initCoverAreas(_this.cropBox, _this.currentCropVariant.coverAreas);
+                }
+                // Check if new cropVariant has focusArea
+                if (_this.currentCropVariant.focusArea) {
+                    // Init or reinit focusArea
+                    if (ImageManipulation.isEmptyArea(_this.currentCropVariant.focusArea)) {
+                        // If an empty focusArea is set initialise it with the default
+                        _this.currentCropVariant.focusArea = $.extend(true, {}, _this.defaultFocusArea);
+                    }
+                    _this.initFocusArea(_this.cropBox);
+                    _this.scaleAndMoveFocusArea(_this.currentCropVariant.focusArea);
+                }
+                if (_this.currentCropVariant.selectedRatio) {
+                    _this.updateAspectRatio(_this.currentCropVariant.allowedAspectRatios[_this.currentCropVariant.selectedRatio]);
+                    // Set data explicitly or updateAspectRatio up-scales the crop
+                    _this.setCropArea(_this.currentCropVariant.cropArea);
+                    _this.currentModal.find("[data-option='" + _this.currentCropVariant.selectedRatio + "']").addClass('active');
+                }
+                _this.cropperCanvas.addClass('is-visible');
+            };
+            this.cropMoveHandler = function (e) {
+                _this.currentCropVariant.cropArea = $.extend(true, _this.currentCropVariant.cropArea, {
+                    height: Math.floor(e.height),
+                    width: Math.floor(e.width),
+                    x: Math.floor(e.x),
+                    y: Math.floor(e.y),
+                });
+                _this.updatePreviewThumbnail(_this.currentCropVariant);
+                _this.updateCropVariantData(_this.currentCropVariant);
+                _this.cropInfo.text(_this.currentCropVariant.cropArea.width + "\u00D7" + _this.currentCropVariant.cropArea.height + " px");
+            };
+            this.cropStartHandler = function () {
+                if (_this.currentCropVariant.focusArea) {
+                    _this.focusArea.draggable('option', 'disabled', true);
+                    _this.focusArea.resizable('option', 'disabled', true);
+                }
+            };
+            /**
+             *
+             */
+            this.cropEndHandler = function () {
+                if (_this.currentCropVariant.focusArea) {
+                    _this.focusArea.draggable('option', 'disabled', false);
+                    _this.focusArea.resizable('option', 'disabled', false);
+                }
+            };
+            // Silence is golden
+            $(window).resize(function () {
+                if (_this.cropper) {
+                    _this.cropper.cropper('destroy');
+                }
+            });
+            this.resizeEnd(function () {
+                if (_this.cropper) {
+                    _this.init();
+                }
+            });
+        }
+        /**
+         * @method isCropAreaEmpty
+         * @desc Checks if an area is set or pristine
+         * @param {Area} area - The area to check
+         * @return {boolean}
+         * @static
+         */
+        ImageManipulation.isEmptyArea = function (area) {
+            return $.isEmptyObject(area);
+        };
+        /**
+         * @method wait
+         * @desc window.setTimeout shim
+         * @param {Function} fn - The function to execute
+         * @param {number} ms - The time in [ms] to wait until execution
+         * @return {boolean}
+         * @public
+         * @static
+         */
+        ImageManipulation.wait = function (fn, ms) {
+            window.setTimeout(fn, ms);
+        };
+        /**
+         * @method toCssPercent
+         * @desc Takes a number, and converts it to CSS percentage length
+         * @param {number} num - The number to convert
+         * @return {string}
+         * @public
+         * @static
+         */
+        ImageManipulation.toCssPercent = function (num) {
+            return num * 100 + "%";
+        };
+        /**
+         * @method serializeCropVariants
+         * @desc Serializes crop variants for persistence or preview
+         * @param {Object} cropVariants
+         * @returns string
+         */
+        ImageManipulation.serializeCropVariants = function (cropVariants) {
+            var omitUnused = function (key, value) {
+                return (key === 'id'
+                    || key === 'title'
+                    || key === 'allowedAspectRatios'
+                    || key === 'coverAreas') ? undefined : value;
+            };
+            return JSON.stringify(cropVariants, omitUnused);
+        };
+        /**
+         * @method initializeTrigger
+         * @desc Assign a handler to .t3js-image-manipulation-trigger.
+         *       Show the modal and kick-off image manipulation
+         * @public
+         */
+        ImageManipulation.prototype.initializeTrigger = function () {
+            var _this = this;
+            var triggerHandler = function (e) {
+                e.preventDefault();
+                _this.trigger = $(e.currentTarget);
+                _this.show();
+            };
+            $('.t3js-image-manipulation-trigger').off('click').click(triggerHandler);
+        };
+        /**
+         * Initialize the cropper modal
+         */
+        ImageManipulation.prototype.initializeCropperModal = function () {
+            var _this = this;
+            var image = this.currentModal.find('#t3js-crop-image');
+            ImagesLoaded(image, function () {
+                var modal = _this.currentModal.find('.modal-dialog');
+                modal.css({ marginLeft: 'auto', marginRight: 'auto' });
+                modal.addClass('modal-image-manipulation modal-resize');
+                Modal.center();
+                setTimeout(function () {
+                    _this.init();
+                }, 100);
+            });
+        };
+        ImageManipulation.prototype.show = function () {
+            var modalTitle = this.trigger.data('modalTitle');
+            var imageUri = this.trigger.data('url');
+            var initCropperModal = this.initializeCropperModal.bind(this);
+            /**
+             * Open modal with image to crop
+             */
+            this.currentModal = Modal.loadUrl(modalTitle, Severity.notice, [], imageUri, initCropperModal, '.modal-content');
+            this.currentModal.addClass('modal-dark');
+        };
+        ImageManipulation.prototype.init = function () {
+            var _this = this;
+            var image = this.currentModal.find('#t3js-crop-image');
+            var imageHeight = $(image).height();
+            var imageWidth = $(image).width();
+            var data = this.trigger.attr('data-crop-variants');
+            if (!data) {
+                throw new TypeError('ImageManipulation: No cropVariants data found for image');
+            }
+            // If we have data already set we assume an internal reinit eg. after resizing
+            this.data = $.isEmptyObject(this.data) ? JSON.parse(data) : this.data;
+            // Initialize our class members
+            this.currentModal.find('.cropper-image-container').css({ height: imageHeight, width: imageWidth });
+            this.cropVariantTriggers = this.currentModal.find('.t3js-crop-variant-trigger');
+            this.activeCropVariantTrigger = this.currentModal.find('.t3js-crop-variant-trigger.is-active');
+            this.cropInfo = this.currentModal.find(this.cropInfoSelector);
+            this.saveButton = this.currentModal.find('[data-method=save]');
+            this.previewButton = this.currentModal.find('[data-method=preview]');
+            this.dismissButton = this.currentModal.find('[data-method=dismiss]');
+            this.resetButton = this.currentModal.find('[data-method=reset]');
+            this.cropperCanvas = this.currentModal.find('#js-crop-canvas');
+            this.aspectRatioTrigger = this.currentModal.find('[data-method=setAspectRatio]');
+            this.currentCropVariant = this.data[this.activeCropVariantTrigger.attr('data-crop-variant-id')];
+            /**
+             * Assign EventListener to cropVariantTriggers
+             */
+            this.cropVariantTriggers.on('click', function (e) {
+                /**
+                 * Is the current cropVariantTrigger is active, bail out.
+                 * Bootstrap doesn't provide this functionality when collapsing the Collaps panels
+                 */
+                if ($(e.currentTarget).hasClass('is-active')) {
+                    e.stopPropagation();
+                    e.preventDefault();
+                    return;
+                }
+                _this.activeCropVariantTrigger.removeClass('is-active');
+                $(e.currentTarget).addClass('is-active');
+                _this.activeCropVariantTrigger = $(e.currentTarget);
+                var cropVariant = _this.data[_this.activeCropVariantTrigger.attr('data-crop-variant-id')];
+                var imageData = _this.cropper.cropper('getImageData');
+                cropVariant.cropArea = _this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
+                _this.currentCropVariant = $.extend(true, {}, cropVariant);
+                _this.update(cropVariant);
+            });
+            /**
+             * Assign EventListener to aspectRatioTrigger
+             */
+            this.aspectRatioTrigger.on('click', function (e) {
+                var ratioId = $(e.currentTarget).attr('data-option');
+                var temp = $.extend(true, {}, _this.currentCropVariant);
+                var ratio = temp.allowedAspectRatios[ratioId];
+                _this.updateAspectRatio(ratio);
+                // Set data explicitly or updateAspectRatio upscales the crop
+                _this.setCropArea(temp.cropArea);
+                _this.currentCropVariant = $.extend(true, {}, temp, { selectedRatio: ratioId });
+                _this.update(_this.currentCropVariant);
+            });
+            /**
+             * Assign EventListener to saveButton
+             */
+            this.saveButton.on('click', function () {
+                _this.save(_this.data);
+            });
+            /**
+             * Assign EventListener to previewButton if preview url exists
+             */
+            if (this.trigger.attr('data-preview-url')) {
+                this.previewButton.on('click', function () {
+                    _this.openPreview(_this.data);
+                });
+            }
+            else {
+                this.previewButton.hide();
+            }
+            /**
+             * Assign EventListener to dismissButton
+             */
+            this.dismissButton.on('click', function () {
+                _this.destroy();
+            });
+            /**
+             * Assign EventListener to resetButton
+             */
+            this.resetButton.on('click', function (e) {
+                var imageData = _this.cropper.cropper('getImageData');
+                var resetCropVariantString = $(e.currentTarget).attr('data-crop-variant');
+                e.preventDefault();
+                e.stopPropagation();
+                if (!resetCropVariantString) {
+                    throw new TypeError('TYPO3 Cropper: No cropVariant data attribute found on reset element.');
+                }
+                var resetCropVariant = JSON.parse(resetCropVariantString);
+                var absoluteCropArea = _this.convertRelativeToAbsoluteCropArea(resetCropVariant.cropArea, imageData);
+                _this.currentCropVariant = $.extend(true, {}, resetCropVariant, { cropArea: absoluteCropArea });
+                _this.update(_this.currentCropVariant);
+            });
+            // If we start without an cropArea, maximize the cropper
+            if (ImageManipulation.isEmptyArea(this.currentCropVariant.cropArea)) {
+                this.defaultOpts = $.extend({
+                    autoCropArea: 1,
+                }, this.defaultOpts);
+            }
+            /**
+             * Initialise the cropper
+             *
+             * Note: We use the extraneous jQuery object here, as CropperJS won't work inside the <iframe>
+             * The top.require is now inlined @see ImageManipulationElemen.php:143
+             * TODO: Find a better solution for cross iframe communications
+             */
+            this.cropper = top.TYPO3.jQuery(image).cropper($.extend(this.defaultOpts, {
+                built: this.cropBuiltHandler,
+                crop: this.cropMoveHandler,
+                cropend: this.cropEndHandler,
+                cropstart: this.cropStartHandler,
+                data: this.currentCropVariant.cropArea,
+            }));
+        };
+        /**
+         * @method update
+         * @desc Update current cropArea position and size when changing cropVariants
+         * @param {CropVariant} cropVariant - The new cropVariant to update the UI with
+         */
+        ImageManipulation.prototype.update = function (cropVariant) {
+            var temp = $.extend(true, {}, cropVariant);
+            var selectedRatio = cropVariant.allowedAspectRatios[cropVariant.selectedRatio];
+            this.currentModal.find('[data-option]').removeClass('active');
+            this.currentModal.find("[data-option=\"" + cropVariant.selectedRatio + "\"]").addClass('active');
+            /**
+             * Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
+             */
+            this.updateAspectRatio(selectedRatio);
+            this.setCropArea(temp.cropArea);
+            this.currentCropVariant = $.extend(true, {}, temp, cropVariant);
+            this.cropBox.find(this.coverAreaSelector).remove();
+            // If the current container has a focus area element, deregister and cleanup prior to initialization
+            if (this.cropBox.has(this.focusAreaSelector).length) {
+                this.focusArea.resizable('destroy').draggable('destroy');
+                this.focusArea.remove();
+            }
+            // Check if new cropVariant has focusArea
+            if (cropVariant.focusArea) {
+                // Init or reinit focusArea
+                if (ImageManipulation.isEmptyArea(cropVariant.focusArea)) {
+                    this.currentCropVariant.focusArea = $.extend(true, {}, this.defaultFocusArea);
+                }
+                this.initFocusArea(this.cropBox);
+                this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea);
+            }
+            // Check if new cropVariant has coverAreas
+            if (cropVariant.coverAreas) {
+                // Init or reinit focusArea
+                this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
+            }
+            this.updatePreviewThumbnail(this.currentCropVariant);
+        };
+        /**
+         * @method initFocusArea
+         * @desc Initializes the focus area inside a container and registers the resizable and draggable interfaces to it
+         * @param container: JQuery
+         */
+        ImageManipulation.prototype.initFocusArea = function (container) {
+            var _this = this;
+            this.focusArea = $('<div id="t3js-cropper-focus-area" class="cropper-focus-area"></div>');
+            container.append(this.focusArea);
+            this.focusArea
+                .draggable({
+                containment: container,
+                create: function () {
+                    _this.scaleAndMoveFocusArea(_this.currentCropVariant.focusArea);
+                },
+                drag: function () {
+                    var _a = container.offset(), left = _a.left, top = _a.top;
+                    var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
+                    var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
+                    focusArea.x = (fLeft - left) / container.width();
+                    focusArea.y = (fTop - top) / container.height();
+                    _this.updatePreviewThumbnail(_this.currentCropVariant);
+                    if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+                        _this.focusArea.addClass('has-nodrop');
+                    }
+                    else {
+                        _this.focusArea.removeClass('has-nodrop');
+                    }
+                },
+                revert: function () {
+                    var revertDelay = 250;
+                    var _a = container.offset(), left = _a.left, top = _a.top;
+                    var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
+                    var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
+                    if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+                        _this.focusArea.removeClass('has-nodrop');
+                        ImageManipulation.wait(function () {
+                            focusArea.x = (fLeft - left) / container.width();
+                            focusArea.y = (fTop - top) / container.height();
+                            _this.updateCropVariantData(_this.currentCropVariant);
+                        }, revertDelay);
+                        return true;
+                    }
+                },
+                revertDuration: 200,
+                stop: function () {
+                    var _a = container.offset(), left = _a.left, top = _a.top;
+                    var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
+                    var focusArea = _this.currentCropVariant.focusArea;
+                    focusArea.x = (fLeft - left) / container.width();
+                    focusArea.y = (fTop - top) / container.height();
+                    _this.scaleAndMoveFocusArea(focusArea);
+                },
+            })
+                .resizable({
+                containment: container,
+                handles: 'all',
+                resize: function () {
+                    var _a = container.offset(), left = _a.left, top = _a.top;
+                    var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
+                    var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
+                    focusArea.height = _this.focusArea.height() / container.height();
+                    focusArea.width = _this.focusArea.width() / container.width();
+                    focusArea.x = (fLeft - left) / container.width();
+                    focusArea.y = (fTop - top) / container.height();
+                    _this.updatePreviewThumbnail(_this.currentCropVariant);
+                    if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+                        _this.focusArea.addClass('has-nodrop');
+                    }
+                    else {
+                        _this.focusArea.removeClass('has-nodrop');
+                    }
+                },
+                stop: function (event, ui) {
+                    var revertDelay = 250;
+                    var _a = container.offset(), left = _a.left, top = _a.top;
+                    var _b = _this.focusArea.offset(), fLeft = _b.left, fTop = _b.top;
+                    var _c = _this.currentCropVariant, focusArea = _c.focusArea, coverAreas = _c.coverAreas;
+                    if (_this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
+                        ui.element.animate($.extend(ui.originalPosition, ui.originalSize), revertDelay, function () {
+                            focusArea.height = _this.focusArea.height() / container.height();
+                            focusArea.height = _this.focusArea.height() / container.height();
+                            focusArea.width = _this.focusArea.width() / container.width();
+                            focusArea.x = (fLeft - left) / container.width();
+                            focusArea.y = (fTop - top) / container.height();
+                            _this.scaleAndMoveFocusArea(focusArea);
+                            _this.focusArea.removeClass('has-nodrop');
+                        });
+                    }
+                    else {
+                        _this.scaleAndMoveFocusArea(focusArea);
+                    }
+                },
+            });
+        };
+        /**
+         * @method initCoverAreas
+         * @desc Initialise cover areas inside the cropper container
+         * @param {JQuery} container - The container element to append the cover areas
+         * @param {Array<Area>} coverAreas - An array of areas to construxt the cover area elements from
+         */
+        ImageManipulation.prototype.initCoverAreas = function (container, coverAreas) {
+            coverAreas.forEach(function (coverArea) {
+                var coverAreaCanvas = $('<div class="t3js-cropper-cover-area"></div>');
+                container.append(coverAreaCanvas);
+                coverAreaCanvas.css({
+                    height: ImageManipulation.toCssPercent(coverArea.height),
+                    left: ImageManipulation.toCssPercent(coverArea.x),
+                    top: ImageManipulation.toCssPercent(coverArea.y),
+                    width: ImageManipulation.toCssPercent(coverArea.width),
+                });
+            });
+        };
+        /**
+         * @method updatePreviewThumbnail
+         * @desc Sync the croping (and focus area) to the preview thumbnail
+         * @param {CropVariant} cropVariant
+         */
+        ImageManipulation.prototype.updatePreviewThumbnail = function (cropVariant) {
+            var styles;
+            var cropperPreviewThumbnailCrop = this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
+            var cropperPreviewThumbnailImage = this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
+            var cropperPreviewThumbnailFocus = this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
+            var imageData = this.cropper.cropper('getImageData');
+            // Update the position/dimension of the crop area in the preview
+            cropperPreviewThumbnailCrop.css({
+                height: ImageManipulation.toCssPercent(cropVariant.cropArea.height / imageData.naturalHeight),
+                left: ImageManipulation.toCssPercent(cropVariant.cropArea.x / imageData.naturalWidth),
+                top: ImageManipulation.toCssPercent(cropVariant.cropArea.y / imageData.naturalHeight),
+                width: ImageManipulation.toCssPercent(cropVariant.cropArea.width / imageData.naturalWidth),
+            });
+            // Show and update focusArea in the preview only if we really have one configured
+            if (cropVariant.focusArea) {
+                cropperPreviewThumbnailFocus.css({
+                    height: ImageManipulation.toCssPercent(cropVariant.focusArea.height),
+                    left: ImageManipulation.toCssPercent(cropVariant.focusArea.x),
+                    top: ImageManipulation.toCssPercent(cropVariant.focusArea.y),
+                    width: ImageManipulation.toCssPercent(cropVariant.focusArea.width),
+                });
+            }
+            // Destruct the preview container's CSS properties
+            styles = cropperPreviewThumbnailCrop.css([
+                'width', 'height', 'left', 'top',
+            ]);
+            /**
+             * Apply negative margins on the previewThumbnailImage to make the illusion of an offset
+             */
+            cropperPreviewThumbnailImage.css({
+                height: parseFloat(styles.height) * (1 / (cropVariant.cropArea.height / imageData.naturalHeight)) + "px",
+                margin: -1 * parseFloat(styles.left) + "px",
+                marginTop: -1 * parseFloat(styles.top) + "px",
+                width: parseFloat(styles.width) * (1 / (cropVariant.cropArea.width / imageData.naturalWidth)) + "px",
+            });
+        };
+        /**
+         * @method scaleAndMoveFocusArea
+         * @desc Calculation logic for moving the focus area given the
+         *       specified constrains of a crop and an optional cover area
+         * @param {Area} focusArea - The translation data
+         */
+        ImageManipulation.prototype.scaleAndMoveFocusArea = function (focusArea) {
+            this.focusArea.css({
+                height: ImageManipulation.toCssPercent(focusArea.height),
+                left: ImageManipulation.toCssPercent(focusArea.x),
+                top: ImageManipulation.toCssPercent(focusArea.y),
+                width: ImageManipulation.toCssPercent(focusArea.width),
+            });
+            this.currentCropVariant.focusArea = focusArea;
+            this.updatePreviewThumbnail(this.currentCropVariant);
+            this.updateCropVariantData(this.currentCropVariant);
+        };
+        /**
+         * @method updateCropVariantData
+         * @desc Immutably updates the currently selected cropVariant data
+         * @param {CropVariant} currentCropVariant - The cropVariant to immutably save
+         */
+        ImageManipulation.prototype.updateCropVariantData = function (currentCropVariant) {
+            var imageData = this.cropper.cropper('getImageData');
+            var absoluteCropArea = this.convertAbsoluteToRelativeCropArea(currentCropVariant.cropArea, imageData);
+            this.data[currentCropVariant.id] = $.extend(true, {}, currentCropVariant, { cropArea: absoluteCropArea });
+        };
+        /**
+         * @method updateAspectRatio
+         * @desc Updates the aspect ratio in the cropper
+         * @param {ratio} ratio ratio set in the cropper
+         */
+        ImageManipulation.prototype.updateAspectRatio = function (ratio) {
+            this.cropper.cropper('setAspectRatio', ratio.value);
+        };
+        /**
+         * @method setCropArea
+         * @desc Updates the crop area in the cropper. The cropper will respect the selected ratio
+         * @param {cropArea} cropArea ratio set in the cropper
+         */
+        ImageManipulation.prototype.setCropArea = function (cropArea) {
+            this.cropper.cropper('setData', {
+                height: cropArea.height,
+                width: cropArea.width,
+                x: cropArea.x,
+                y: cropArea.y,
+            });
+        };
+        /**
+         * @method checkFocusAndCoverAreas
+         * @desc Checks is one focus area and one or more cover areas overlap
+         * @param focusArea
+         * @param coverAreas
+         * @return {boolean}
+         */
+        ImageManipulation.prototype.checkFocusAndCoverAreasCollision = function (focusArea, coverAreas) {
+            return coverAreas
+                .some(function (coverArea) {
+                // noinspection OverlyComplexBooleanExpressionJS
+                if (focusArea.x < coverArea.x + coverArea.width &&
+                    focusArea.x + focusArea.width > coverArea.x &&
+                    focusArea.y < coverArea.y + coverArea.height &&
+                    focusArea.height + focusArea.y > coverArea.y) {
+                    return true;
+                }
+            });
+        };
+        /**
+         * @param cropArea
+         * @param imageData
+         * @return {{height: number, width: number, x: number, y: number}}
+         */
+        ImageManipulation.prototype.convertAbsoluteToRelativeCropArea = function (cropArea, imageData) {
+            var height = cropArea.height, width = cropArea.width, x = cropArea.x, y = cropArea.y;
+            return {
+                height: height / imageData.naturalHeight,
+                width: width / imageData.naturalWidth,
+                x: x / imageData.naturalWidth,
+                y: y / imageData.naturalHeight,
+            };
+        };
+        /**
+         * @param cropArea
+         * @param imageData
+         * @return {{height: number, width: number, x: number, y: number}}
+         */
+        ImageManipulation.prototype.convertRelativeToAbsoluteCropArea = function (cropArea, imageData) {
+            var height = cropArea.height, width = cropArea.width, x = cropArea.x, y = cropArea.y;
+            return {
+                height: height * imageData.naturalHeight,
+                width: width * imageData.naturalWidth,
+                x: x * imageData.naturalWidth,
+                y: y * imageData.naturalHeight,
+            };
+        };
+        ImageManipulation.prototype.setPreviewImage = function (data) {
+            var _this = this;
+            var $image = this.cropper;
+            var imageData = $image.cropper('getImageData');
+            Object.keys(data).forEach(function (cropVariantId) {
+                var cropVariant = data[cropVariantId];
+                var cropData = _this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
+                var $preview = _this.trigger
+                    .closest('.form-group')
+                    .find(".t3js-image-manipulation-preview[data-crop-variant-id=\"" + cropVariantId + "\"]");
+                if ($preview.length === 0) {
+                    return;
+                }
+                var previewWidth = $preview.data('preview-width');
+                var previewHeight = $preview.data('preview-height');
+                // Adjust aspect ratio of preview width/height
+                var aspectRatio = cropData.width / cropData.height;
+                var tmpHeight = previewWidth / aspectRatio;
+                if (tmpHeight > previewHeight) {
+                    previewWidth = previewHeight * aspectRatio;
+                }
+                else {
+                    previewHeight = tmpHeight;
+                }
+                // preview should never be up-scaled
+                if (previewWidth > cropData.width) {
+                    previewWidth = cropData.width;
+                    previewHeight = cropData.height;
+                }
+                var ratio = previewWidth / cropData.width;
+                var $viewBox = $('<div />').html('<img src="' + $image.attr('src') + '">');
+                $viewBox.addClass('cropper-preview-container');
+                $preview.empty().append($viewBox);
+                $viewBox.wrap('<span class="thumbnail thumbnail-status"></span>');
+                $viewBox.width(previewWidth).height(previewHeight).find('img').css({
+                    height: imageData.naturalHeight * ratio,
+                    left: -cropData.x * ratio,
+                    top: -cropData.y * ratio,
+                    width: imageData.naturalWidth * ratio,
+                });
+            });
+        };
+        ;
+        /**
+         * @method openPreview
+         * @desc open a preview
+         * @param {object} data - The whole data object containing all the cropVariants
+         * @private
+         */
+        ImageManipulation.prototype.openPreview = function (data) {
+            var cropVariants = ImageManipulation.serializeCropVariants(data);
+            var previewUrl = this.trigger.attr('data-preview-url');
+            previewUrl = previewUrl + '&cropVariants=' + encodeURIComponent(cropVariants);
+            window.open(previewUrl, 'TYPO3ImageManipulationPreview');
+        };
+        /**
+         * @method save
+         * @desc Saves the edited cropVariants to a hidden field
+         * @param {object} data - The whole data object containing all the cropVariants
+         * @private
+         */
+        ImageManipulation.prototype.save = function (data) {
+            var cropVariants = ImageManipulation.serializeCropVariants(data);
+            var hiddenField = $("#" + this.trigger.attr('data-field'));
+            this.trigger.attr('data-crop-variants', JSON.stringify(data));
+            this.setPreviewImage(data);
+            hiddenField.val(cropVariants);
+            this.destroy();
+        };
+        /**
+         * @method destroy
+         * @desc Destroy the ImageManipulation including cropper and alike
+         * @private
+         */
+        ImageManipulation.prototype.destroy = function () {
+            if (this.currentModal) {
+                this.currentModal.modal('hide');
+                this.cropper.cropper('destroy');
+                this.currentModal = null;
+            }
+        };
+        ImageManipulation.prototype.resizeEnd = function (fn) {
+            var timer;
+            $(window).on('resize', function () {
+                clearTimeout(timer);
+                timer = setTimeout(function () {
+                    fn();
+                }, 450);
+            });
+        };
+        return ImageManipulation;
+    }());
+    return new ImageManipulation();
 });
index 1bc60df..f1b90a0 100644 (file)
   z-index: 1;
   margin-left: 1em;
   transition: margin 0.3s;
-  -webkit-filter: grayscale(100%);
-          filter: grayscale(100%);
+  filter: grayscale(100%);
 }
 #t3-form-stage-container.t3-form-stage-viewmode-abstract .t3-form-validator-info .t3-form-validator-list {
   position: absolute;
 }
 #t3-form-stage-container.t3-form-stage-viewmode-abstract #t3-form-stage .t3-form-form-element-selected .t3-form-validator-info .t3-form-icon {
   margin-right: 75px;
-  -webkit-filter: none;
-          filter: none;
+  filter: none;
 }
 #t3-form-stage-container.t3-form-stage-viewmode-abstract #t3-form-stage .t3-form-form-element-selected .btn-toolbar-container {
   position: absolute;
index 8623ba7..64c97a1 100644 (file)
@@ -318,8 +318,7 @@ th {
 :root .fa-rotate-270,
 :root .fa-flip-horizontal,
 :root .fa-flip-vertical {
-  -webkit-filter: none;
-          filter: none;
+  filter: none;
 }
 .fa-stack {
   position: relative;