Check whether the `cropper-selection` in `MinMaxImageCropper` limits to the minimum...
authorCyperghost <olaf_schmitz_1@t-online.de>
Wed, 6 Nov 2024 09:04:39 +0000 (10:04 +0100)
committerCyperghost <olaf_schmitz_1@t-online.de>
Wed, 6 Nov 2024 09:04:39 +0000 (10:04 +0100)
ts/WoltLabSuite/Core/Component/Image/Cropper.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js
wcfsetup/install/files/style/ui/dialog.scss

index d77a980f319f849f1fe3752e2f6bbb6c99072675..a449ffc0a0b945cfee3d2325dde10248058a30c8 100644 (file)
@@ -24,7 +24,7 @@ abstract class ImageCropper {
   protected cropperCanvas?: CropperCanvas | null;
   protected cropperImage?: CropperImage | null;
   protected cropperSelection?: CropperSelection | null;
-  protected dialog: WoltlabCoreDialogElement;
+  protected dialog?: WoltlabCoreDialogElement;
   protected exif?: ExifUtil.Exif;
   #cropper?: Cropper;
 
@@ -33,6 +33,14 @@ abstract class ImageCropper {
     this.element = element;
     this.file = file;
     this.resizer = new ImageResizer();
+
+    this.configuration.sizes = this.configuration.sizes.sort((a, b) => {
+      if (a.width >= a.height) {
+        return b.width - a.width;
+      } else {
+        return b.height - a.height;
+      }
+    });
   }
 
   public async showDialog(): Promise<File> {
@@ -43,13 +51,8 @@ abstract class ImageCropper {
 
     this.createCropper();
 
-    this.dialog.addEventListener("extra", () => {
-      this.cropperImage!.$center("contain");
-      this.cropperSelection!.$reset();
-    });
-
     return new Promise<File>((resolve, reject) => {
-      this.dialog.addEventListener("primary", () => {
+      this.dialog!.addEventListener("primary", () => {
         this.cropperSelection!.$toCanvas()
           .then((canvas) => {
             this.resizer
@@ -157,14 +160,6 @@ class ExactImageCropper extends ImageCropper {
     });
 
     // resize image to the largest possible size
-    this.configuration.sizes = this.configuration.sizes.sort((a, b) => {
-      if (a.width >= a.height) {
-        return b.width - a.width;
-      } else {
-        return b.height - a.height;
-      }
-    });
-
     const sizes = this.configuration.sizes.filter((size) => {
       return size.width <= this.image!.width && size.height <= this.image!.height;
     });
@@ -218,13 +213,32 @@ class ExactImageCropper extends ImageCropper {
 }
 
 class MinMaxImageCropper extends ImageCropper {
+  constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) {
+    super(element, file, configuration);
+    if (configuration.sizes.length !== 2) {
+      throw new Error("MinMaxImageCropper requires exactly two sizes");
+    }
+  }
+
+  get minSize() {
+    return this.configuration.sizes[1];
+  }
+
+  get maxSize() {
+    return this.configuration.sizes[0];
+  }
+
+  protected getDialogExtra(): string {
+    return getPhrase("wcf.global.button.reset");
+  }
+
   protected getCropperTemplate(): string {
     return `<div class="cropperContainer">
   <cropper-canvas background>
     <cropper-image skewable scalable translatable></cropper-image>
     <cropper-shade hidden></cropper-shade>
     <cropper-handle action="move" plain></cropper-handle>
-    <cropper-selection initial-coverage="0.5" movable resizable outlined>
+    <cropper-selection movable zoomable resizable outlined>
       <cropper-grid role="grid" bordered covered></cropper-grid>
       <cropper-crosshair centered></cropper-crosshair>
       <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
@@ -241,11 +255,37 @@ class MinMaxImageCropper extends ImageCropper {
 </div>`;
   }
 
-  protected getDialogExtra(): string {
-    return getPhrase("wcf.global.button.reset");
+  protected setCropperStyle() {
+    super.setCropperStyle();
+
+    this.cropperSelection!.width = this.minSize.width;
+    this.cropperSelection!.height = this.minSize.height;
+    this.cropperCanvas!.style.minWidth = `min(${this.maxSize.width}px, ${this.image!.width}px)`;
+    this.cropperCanvas!.style.minHeight = `min(${this.maxSize.height}px, ${this.image!.height}px)`;
   }
 
-  // TODO handle resize cropper selection to min/max size
+  protected createCropper() {
+    super.createCropper();
+
+    this.dialog!.addEventListener("extra", () => {
+      this.cropperImage!.$center("contain");
+      this.cropperSelection!.$reset();
+    });
+
+    // Limit the selection to the canvas boundaries
+    this.cropperSelection!.addEventListener("change", (event: CustomEvent) => {
+      const selection = event.detail as Selection;
+
+      if (
+        selection.width < this.minSize.width ||
+        selection.height < this.minSize.height ||
+        selection.width > this.maxSize.width ||
+        selection.height > this.maxSize.height
+      ) {
+        event.preventDefault();
+      }
+    });
+  }
 }
 
 export async function cropImage(
index 4139d310bf2f4a1a912ce44ed08c0d0c18326916..524384bcfac9fe016e21c233b58023938c3d2109 100644 (file)
@@ -21,6 +21,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL
             this.element = element;
             this.file = file;
             this.resizer = new Resizer_1.default();
+            this.configuration.sizes = this.configuration.sizes.sort((a, b) => {
+                if (a.width >= a.height) {
+                    return b.width - a.width;
+                }
+                else {
+                    return b.height - a.height;
+                }
+            });
         }
         async showDialog() {
             this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({
@@ -28,10 +36,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL
             });
             this.dialog.show((0, Language_1.getPhrase)("wcf.upload.crop.image"));
             this.createCropper();
-            this.dialog.addEventListener("extra", () => {
-                this.cropperImage.$center("contain");
-                this.cropperSelection.$reset();
-            });
             return new Promise((resolve, reject) => {
                 this.dialog.addEventListener("primary", () => {
                     this.cropperSelection.$toCanvas()
@@ -118,14 +122,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL
                 window.setTimeout(() => resolve(this.file), 10_000);
             });
             // resize image to the largest possible size
-            this.configuration.sizes = this.configuration.sizes.sort((a, b) => {
-                if (a.width >= a.height) {
-                    return b.width - a.width;
-                }
-                else {
-                    return b.height - a.height;
-                }
-            });
             const sizes = this.configuration.sizes.filter((size) => {
                 return size.width <= this.image.width && size.height <= this.image.height;
             });
@@ -162,13 +158,28 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL
         }
     }
     class MinMaxImageCropper extends ImageCropper {
+        constructor(element, file, configuration) {
+            super(element, file, configuration);
+            if (configuration.sizes.length !== 2) {
+                throw new Error("MinMaxImageCropper requires exactly two sizes");
+            }
+        }
+        get minSize() {
+            return this.configuration.sizes[1];
+        }
+        get maxSize() {
+            return this.configuration.sizes[0];
+        }
+        getDialogExtra() {
+            return (0, Language_1.getPhrase)("wcf.global.button.reset");
+        }
         getCropperTemplate() {
             return `<div class="cropperContainer">
   <cropper-canvas background>
     <cropper-image skewable scalable translatable></cropper-image>
     <cropper-shade hidden></cropper-shade>
     <cropper-handle action="move" plain></cropper-handle>
-    <cropper-selection initial-coverage="0.5" movable resizable outlined>
+    <cropper-selection movable zoomable resizable outlined>
       <cropper-grid role="grid" bordered covered></cropper-grid>
       <cropper-crosshair centered></cropper-crosshair>
       <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
@@ -184,8 +195,29 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL
   </cropper-canvas>
 </div>`;
         }
-        getDialogExtra() {
-            return (0, Language_1.getPhrase)("wcf.global.button.reset");
+        setCropperStyle() {
+            super.setCropperStyle();
+            this.cropperSelection.width = this.minSize.width;
+            this.cropperSelection.height = this.minSize.height;
+            this.cropperCanvas.style.minWidth = `min(${this.maxSize.width}px, ${this.image.width}px)`;
+            this.cropperCanvas.style.minHeight = `min(${this.maxSize.height}px, ${this.image.height}px)`;
+        }
+        createCropper() {
+            super.createCropper();
+            this.dialog.addEventListener("extra", () => {
+                this.cropperImage.$center("contain");
+                this.cropperSelection.$reset();
+            });
+            // Limit the selection to the canvas boundaries
+            this.cropperSelection.addEventListener("change", (event) => {
+                const selection = event.detail;
+                if (selection.width < this.minSize.width ||
+                    selection.height < this.minSize.height ||
+                    selection.width > this.maxSize.width ||
+                    selection.height > this.maxSize.height) {
+                    event.preventDefault();
+                }
+            });
         }
     }
     async function cropImage(element, file, configuration) {
index dfc88c79a212eced9fe2d5cb8117a7768e4da6c6..e4a5ae0a71b9bca358a6290c1f6d1cce1d94fa87 100644 (file)
@@ -467,5 +467,7 @@ html[data-color-scheme="dark"] .dialog::backdrop {
 }
 
 .dialog .cropperContainer {
-       overflow: scroll;
+       overflow: auto;
+       height: 100%;
+       width: 100%;
 }