From 3ba50c3a44544ba9386484412b207d5b89021b7d Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Tue, 15 Nov 2022 11:51:35 +0100
Subject: [PATCH] [FEATURE] Allow static backdrops for modals

The Modal API is now able to render a static backdrop to avoid closing
the modal when clicking it which may come in handy in case closing the
modal would result in a negative user experience, e.g. in the image
cropper.

The new boolean configuration option `staticBackdrop` controls whether
a static backdrop should be rendered or not, defaulting to `false`.

The trigger class "t3js-modal-trigger" also supports the new option
by setting corresponding data attribute.

Resolves: #99092
Releases: main
Change-Id: I0e1c7753944b41d5266aa6232d1fa838752f95b3
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76605
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
---
 .../TypeScript/backend/image-manipulation.ts  |  3 +-
 Build/Sources/TypeScript/backend/modal.ts     |  7 +++
 .../Public/JavaScript/image-manipulation.js   |  2 +-
 .../Resources/Public/JavaScript/modal.js      |  5 +-
 ...ure-99092-AllowStaticBackdropsInModals.rst | 53 +++++++++++++++++++
 5 files changed, 65 insertions(+), 5 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.1/Feature-99092-AllowStaticBackdropsInModals.rst

diff --git a/Build/Sources/TypeScript/backend/image-manipulation.ts b/Build/Sources/TypeScript/backend/image-manipulation.ts
index 26710706e6fa..cb2b513ab160 100644
--- a/Build/Sources/TypeScript/backend/image-manipulation.ts
+++ b/Build/Sources/TypeScript/backend/image-manipulation.ts
@@ -230,6 +230,7 @@ class ImageManipulation {
       size: Modal.sizes.full,
       style: Modal.styles.dark,
       title: modalTitle,
+      staticBackdrop: true
     });
 
     this.currentModal.addEventListener('typo3-modal-shown', (): void => {
@@ -243,8 +244,6 @@ class ImageManipulation {
     this.currentModal.addEventListener('typo3-modal-hide', (): void => {
       this.destroy();
     });
-    // do not dismiss the modal when clicking beside it to avoid data loss
-    this.currentModal.style.pointerEvents = 'none';
   }
 
   /**
diff --git a/Build/Sources/TypeScript/backend/modal.ts b/Build/Sources/TypeScript/backend/modal.ts
index 2c4c0a56b099..2929fda5faa2 100644
--- a/Build/Sources/TypeScript/backend/modal.ts
+++ b/Build/Sources/TypeScript/backend/modal.ts
@@ -82,6 +82,7 @@ export interface Configuration {
   additionalCssClasses: Array<string>;
   callback: ModalCallbackFunction | null;
   ajaxCallback: ModalCallbackFunction | null;
+  staticBackdrop: boolean;
 }
 
 type PartialConfiguration = Partial<Omit<Configuration, 'buttons'> & { buttons: Array<Partial<Button>> }>
@@ -95,6 +96,7 @@ export class ModalElement extends LitElement {
   @property({type: String, reflect: true}) variant: Styles = Styles.default;
   @property({type: String, reflect: true}) size: Sizes = Sizes.default;
   @property({type: Number, reflect: true}) zindex: Number = 5000;
+  @property({type: Boolean}) staticBackdrop: boolean = false;
   @property({type: Array}) additionalCssClasses: Array<string> = [];
   @property({type: Array, attribute: false}) buttons: Array<Button> = [];
 
@@ -141,6 +143,7 @@ export class ModalElement extends LitElement {
           tabindex="-1"
           class="modal fade t3js-modal ${classMap(classes)}"
           style=${styleMap(styles)}
+          data-bs-backdrop="${ifDefined(this.staticBackdrop) ? 'static' : true}"
           @show.bs.modal=${() => this.trigger('typo3-modal-show')}
           @shown.bs.modal=${() => this.trigger('typo3-modal-shown')}
           @hide.bs.modal=${() => this.trigger('typo3-modal-hide')}
@@ -277,6 +280,7 @@ class Modal {
     additionalCssClasses: [],
     callback: null,
     ajaxCallback: null,
+    staticBackdrop: false
   };
 
   private static createModalResponseEventFromElement(element: HTMLElement, result: boolean): ModalResponseEvent | null {
@@ -445,6 +449,7 @@ class Modal {
     configuration.ajaxCallback = typeof configuration.ajaxCallback === 'function'
       ? configuration.ajaxCallback
       : this.defaultConfiguration.ajaxCallback;
+    configuration.staticBackdrop = configuration.staticBackdrop || this.defaultConfiguration.staticBackdrop;
 
     return this.generate(configuration);
   }
@@ -479,6 +484,7 @@ class Modal {
         title: triggerElement.dataset.title || 'Alert',
         content: url !== null ? url : content,
         severity,
+        staticBackdrop: triggerElement.dataset.staticBackdrop !== undefined,
         buttons: [
           {
             text: triggerElement.dataset.buttonCloseText || TYPO3.lang['button.close'] || 'Close',
@@ -546,6 +552,7 @@ class Modal {
     currentModal.modalTitle = configuration.title;
     currentModal.additionalCssClasses = configuration.additionalCssClasses;
     currentModal.buttons = <Array<Button>>configuration.buttons;
+    currentModal.staticBackdrop = configuration.staticBackdrop;
     if (configuration.callback) {
       currentModal.callback = configuration.callback;
     }
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js b/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js
index 8724212bf75e..c22771102ddd 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import $ from"jquery";import{html}from"lit";import{unsafeHTML}from"lit/directives/unsafe-html.js";import"jquery-ui/draggable.js";import"jquery-ui/resizable.js";import FormEngineValidation from"@typo3/backend/form-engine-validation.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Cropper from"cropperjs";import{default as Modal}from"@typo3/backend/modal.js";import"@typo3/backend/element/spinner-element.js";class ImageManipulation{constructor(){this.initialized=!1,this.cropImageContainerSelector="#t3js-crop-image-container",this.cropImageSelector="#t3js-crop-image",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:!0,autoCropArea:.7,dragMode:"crop",guides:!0,responsive:!0,viewMode:1,zoomable:!1,checkCrossOrigin:!1},this.cropBuiltHandler=()=>{this.initialized=!0;const t=this.cropper.getImageData(),e=this.currentModal.querySelector(this.cropImageSelector);$(this.currentModal).find(".cropper-canvas img").removeClass("cropper-hide"),this.imageOriginalSizeFactor=parseInt(e.dataset.originalWidth,10)/t.naturalWidth,this.cropVariantTriggers.each(((e,r)=>{const a=$(r).attr("data-crop-variant-id"),i=this.convertRelativeToAbsoluteCropArea(this.data[a].cropArea,t),o=$.extend(!0,{},this.data[a],{cropArea:i});this.updatePreviewThumbnail(o,$(r))})),this.currentCropVariant.cropArea=this.convertRelativeToAbsoluteCropArea(this.currentCropVariant.cropArea,t),this.cropBox=$(this.currentModal).find(".cropper-crop-box"),this.setCropArea(this.currentCropVariant.cropArea),this.currentCropVariant.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.currentCropVariant.focusArea&&(ImageManipulation.isEmptyArea(this.currentCropVariant.focusArea)&&(this.currentCropVariant.focusArea=$.extend(!0,{},this.defaultFocusArea)),this.initFocusArea(this.cropBox),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)),this.currentCropVariant.selectedRatio&&$(this.currentModal).find(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`).addClass("active")},this.cropMoveHandler=t=>{if(!this.initialized)return;this.currentCropVariant.cropArea=$.extend(!0,this.currentCropVariant.cropArea,{height:Math.floor(t.detail.height),width:Math.floor(t.detail.width),x:Math.floor(t.detail.x),y:Math.floor(t.detail.y)}),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant);const e=Math.round(this.currentCropVariant.cropArea.width*this.imageOriginalSizeFactor),r=Math.round(this.currentCropVariant.cropArea.height*this.imageOriginalSizeFactor);this.cropInfo.text(`${e}×${r} px`)},this.cropStartHandler=()=>{this.currentCropVariant.focusArea&&(this.focusArea.draggable("option","disabled",!0),this.focusArea.resizable("option","disabled",!0))},this.cropEndHandler=()=>{this.currentCropVariant.focusArea&&(this.focusArea.draggable("option","disabled",!1),this.focusArea.resizable("option","disabled",!1))}}static isEmptyArea(t){return $.isEmptyObject(t)}static wait(t,e){window.setTimeout(t,e)}static toCssPercent(t){return 100*t+"%"}static serializeCropVariants(t){return JSON.stringify(t,((t,e)=>"id"===t||"title"===t||"allowedAspectRatios"===t||"coverAreas"===t?void 0:e))}initializeTrigger(){$(".t3js-image-manipulation-trigger").off("click").on("click",(t=>{t.preventDefault(),this.trigger=$(t.currentTarget),this.show()}))}async initializeCropperModal(){const t=this.currentModal.querySelector(this.cropImageSelector);await new Promise((e=>{t.complete?e():t.addEventListener("load",(()=>e()))})),this.init()}show(){const t=this.trigger.data("modalTitle"),e=this.trigger.data("buttonPreviewText"),r=this.trigger.data("buttonDismissText"),a=this.trigger.data("buttonSaveText"),i=this.trigger.data("url"),o=this.trigger.data("payload");this.currentModal=Modal.advanced({additionalCssClasses:["modal-image-manipulation","cropper"],buttons:[{btnClass:"btn-default float-start",name:"preview",icon:"actions-view",text:e},{btnClass:"btn-default",name:"dismiss",icon:"actions-close",text:r},{btnClass:"btn-primary",name:"save",icon:"actions-document-save",text:a}],content:html`<div class="modal-loading"><typo3-backend-spinner size="default"></typo3-backend-spinner></div>`,size:Modal.sizes.full,style:Modal.styles.dark,title:t}),this.currentModal.addEventListener("typo3-modal-shown",(()=>{new AjaxRequest(i).post(o).then((async t=>{const e=await t.resolve();this.currentModal.templateResultContent=html`${unsafeHTML(e)}`,this.currentModal.updateComplete.then((()=>this.initializeCropperModal()))}))})),this.currentModal.addEventListener("typo3-modal-hide",(()=>{this.destroy()})),this.currentModal.style.pointerEvents="none"}init(){const t=this.currentModal.querySelector(this.cropImageSelector),e=this.trigger.attr("data-crop-variants");if(!e)throw new TypeError("ImageManipulation: No cropVariants data found for image");this.data=$.isEmptyObject(this.data)?JSON.parse(e):this.data,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("button[name=save]"),this.previewButton=$(this.currentModal).find("button[name=preview]"),this.dismissButton=$(this.currentModal).find("button[name=dismiss]"),this.resetButton=$(this.currentModal).find("button[name=reset]"),this.aspectRatioTrigger=$(this.currentModal).find("label[data-method=setAspectRatio]"),this.currentCropVariant=this.data[this.activeCropVariantTrigger.attr("data-crop-variant-id")],this.cropVariantTriggers.off("click").on("click",(t=>{if($(t.currentTarget).hasClass("is-active"))return t.stopPropagation(),void t.preventDefault();this.activeCropVariantTrigger.removeClass("is-active"),$(t.currentTarget).addClass("is-active"),this.activeCropVariantTrigger=$(t.currentTarget);const e=this.data[this.activeCropVariantTrigger.attr("data-crop-variant-id")],r=this.cropper.getImageData();e.cropArea=this.convertRelativeToAbsoluteCropArea(e.cropArea,r),this.currentCropVariant=$.extend(!0,{},e),this.update(e)})),this.aspectRatioTrigger.off("click").on("click",(t=>{const e=$(t.currentTarget).attr("data-bs-option"),r=$.extend(!0,{},this.currentCropVariant),a=r.allowedAspectRatios[e];this.setAspectRatio(a),this.setCropArea(r.cropArea),this.currentCropVariant=$.extend(!0,{},r,{selectedRatio:e}),this.update(this.currentCropVariant)})),this.saveButton.off("click").on("click",(()=>{this.save(this.data)})),this.trigger.attr("data-preview-url")?this.previewButton.off("click").on("click",(()=>{this.openPreview(this.data)})):this.previewButton.hide(),this.dismissButton.off("click").on("click",(()=>{this.currentModal.hideModal()})),this.resetButton.off("click").on("click",(t=>{const e=this.cropper.getImageData(),r=$(t.currentTarget).attr("data-crop-variant");if(t.preventDefault(),t.stopPropagation(),!r)throw new TypeError("TYPO3 Cropper: No cropVariant data attribute found on reset element.");const a=JSON.parse(r),i=this.convertRelativeToAbsoluteCropArea(a.cropArea,e);this.currentCropVariant=$.extend(!0,{},a,{cropArea:i}),this.update(this.currentCropVariant)})),ImageManipulation.isEmptyArea(this.currentCropVariant.cropArea)&&(this.defaultOpts=$.extend({autoCropArea:1},this.defaultOpts)),this.cropper=new Cropper(t,$.extend(this.defaultOpts,{ready:this.cropBuiltHandler,crop:this.cropMoveHandler,cropend:this.cropEndHandler,cropstart:this.cropStartHandler,data:this.currentCropVariant.cropArea})),this.update(this.currentCropVariant)}update(t){const e=$.extend(!0,{},t),r=t.allowedAspectRatios[t.selectedRatio];$(this.currentModal).find("[data-bs-option]").removeClass("active"),$(this.currentModal).find(`[data-bs-option="${t.selectedRatio}"]`).addClass("active"),this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=$.extend(!0,{},e,t),this.cropBox.find(this.coverAreaSelector).remove(),this.cropBox.has(this.focusAreaSelector).length&&(this.focusArea.resizable("destroy").draggable("destroy"),this.focusArea.remove()),t.focusArea&&(ImageManipulation.isEmptyArea(t.focusArea)&&(this.currentCropVariant.focusArea=$.extend(!0,{},this.defaultFocusArea)),this.initFocusArea(this.cropBox),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)),t.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger)}initFocusArea(t){this.focusArea=$('<div id="t3js-cropper-focus-area" class="cropper-focus-area"></div>'),t.append(this.focusArea),this.focusArea.draggable({containment:t,create:()=>{const t="undefined"!=typeof window&&void 0!==window.document,e=!(!t||!window.document.documentElement)&&"ontouchstart"in window.document.documentElement,r=!!t&&"PointerEvent"in window,a=r?"pointerdown":e?"touchstart":"mousedown",i=r?"pointerup pointercancel":e?"touchend touchcancel":"mouseup";this.focusArea.on(a,(()=>{this.cropper.disable()})),this.focusArea.on(i,(()=>{this.cropper.enable()})),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)},drag:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o,coverAreas:s}=this.currentCropVariant;o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.checkFocusAndCoverAreasCollision(o,s)?this.focusArea.addClass("has-nodrop"):this.focusArea.removeClass("has-nodrop")},revert:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o,coverAreas:s}=this.currentCropVariant;return!!this.checkFocusAndCoverAreasCollision(o,s)&&(this.focusArea.removeClass("has-nodrop"),ImageManipulation.wait((()=>{o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.updateCropVariantData(this.currentCropVariant)}),250),!0)},revertDuration:200,stop:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o}=this.currentCropVariant;o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.scaleAndMoveFocusArea(o)}}).resizable({containment:t,handles:"all",resize:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o,coverAreas:s}=this.currentCropVariant;o.height=this.focusArea.height()/t.height(),o.width=this.focusArea.width()/t.width(),o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.checkFocusAndCoverAreasCollision(o,s)?this.focusArea.addClass("has-nodrop"):this.focusArea.removeClass("has-nodrop")},stop:(e,r)=>{const{left:a,top:i}=t.offset(),{left:o,top:s}=this.focusArea.offset(),{focusArea:n,coverAreas:c}=this.currentCropVariant;this.checkFocusAndCoverAreasCollision(n,c)?r.element.animate($.extend(r.originalPosition,r.originalSize),250,(()=>{n.height=this.focusArea.height()/t.height(),n.width=this.focusArea.width()/t.width(),n.x=(o-a)/t.width(),n.y=(s-i)/t.height(),this.scaleAndMoveFocusArea(n),this.focusArea.removeClass("has-nodrop")})):this.scaleAndMoveFocusArea(n)}})}initCoverAreas(t,e){e.forEach((e=>{const r=$('<div class="cropper-cover-area t3js-cropper-cover-area"></div>');t.append(r),r.css({height:ImageManipulation.toCssPercent(e.height),left:ImageManipulation.toCssPercent(e.x),top:ImageManipulation.toCssPercent(e.y),width:ImageManipulation.toCssPercent(e.width)})}))}updatePreviewThumbnail(t,e){let r;const a=e.find(".t3js-cropper-preview-thumbnail-crop-area"),i=e.find(".t3js-cropper-preview-thumbnail-crop-image"),o=e.find(".t3js-cropper-preview-thumbnail-focus-area"),s=this.cropper.getImageData();a.css({height:ImageManipulation.toCssPercent(t.cropArea.height/s.naturalHeight),left:ImageManipulation.toCssPercent(t.cropArea.x/s.naturalWidth),top:ImageManipulation.toCssPercent(t.cropArea.y/s.naturalHeight),width:ImageManipulation.toCssPercent(t.cropArea.width/s.naturalWidth)}),t.focusArea&&o.css({height:ImageManipulation.toCssPercent(t.focusArea.height),left:ImageManipulation.toCssPercent(t.focusArea.x),top:ImageManipulation.toCssPercent(t.focusArea.y),width:ImageManipulation.toCssPercent(t.focusArea.width)}),r=a.css(["width","height","left","top"]),i.css({height:parseFloat(r.height)*(1/(t.cropArea.height/s.naturalHeight))+"px",margin:-1*parseFloat(r.left)+"px",marginTop:-1*parseFloat(r.top)+"px",width:parseFloat(r.width)*(1/(t.cropArea.width/s.naturalWidth))+"px"})}scaleAndMoveFocusArea(t){this.focusArea.css({height:ImageManipulation.toCssPercent(t.height),left:ImageManipulation.toCssPercent(t.x),top:ImageManipulation.toCssPercent(t.y),width:ImageManipulation.toCssPercent(t.width)}),this.currentCropVariant.focusArea=t,this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant)}updateCropVariantData(t){const e=this.cropper.getImageData(),r=this.convertAbsoluteToRelativeCropArea(t.cropArea,e);this.data[t.id]=$.extend(!0,{},t,{cropArea:r})}setAspectRatio(t){this.cropper.setAspectRatio(t.value)}setCropArea(t){const e=this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];0===e.value?this.cropper.setData({height:t.height,width:t.width,x:t.x,y:t.y}):this.cropper.setData({height:t.height,width:t.height*e.value,x:t.x,y:t.y})}checkFocusAndCoverAreasCollision(t,e){return!!e&&e.some((e=>t.x<e.x+e.width&&t.x+t.width>e.x&&t.y<e.y+e.height&&t.height+t.y>e.y))}convertAbsoluteToRelativeCropArea(t,e){const{height:r,width:a,x:i,y:o}=t;return{height:r/e.naturalHeight,width:a/e.naturalWidth,x:i/e.naturalWidth,y:o/e.naturalHeight}}convertRelativeToAbsoluteCropArea(t,e){const{height:r,width:a,x:i,y:o}=t;return{height:r*e.naturalHeight,width:a*e.naturalWidth,x:i*e.naturalWidth,y:o*e.naturalHeight}}setPreviewImages(t){const e=this.cropper.image,r=this.cropper.getImageData();Object.keys(t).forEach((a=>{const i=t[a],o=this.convertRelativeToAbsoluteCropArea(i.cropArea,r),s=this.trigger.closest(".form-group").find(`.t3js-image-manipulation-preview[data-crop-variant-id="${a}"]`),n=this.trigger.closest(".form-group").find(`.t3js-image-manipulation-selected-ratio[data-crop-variant-id="${a}"]`);if(0===s.length)return;let c=s.width(),h=s.data("preview-height");const p=o.width/o.height,d=c/p;d>h?c=h*p:h=d,c>o.width&&(c=o.width,h=o.height);const l=c/o.width,u=$("<div />").html('<img src="'+e.src+'">'),g=$(this.currentModal).find(`.t3-js-ratio-title[data-ratio-id="${i.id}${i.selectedRatio}"]`);n.text(g.text()),u.addClass("cropper-preview-container"),s.empty().append(u),u.wrap('<span class="thumbnail thumbnail-status"></span>'),u.width(c).height(h).find("img").css({height:r.naturalHeight*l,left:-o.x*l,top:-o.y*l,width:r.naturalWidth*l})}))}openPreview(t){const e=ImageManipulation.serializeCropVariants(t);let r=this.trigger.attr("data-preview-url");r=r+(r.includes("?")?"&":"?")+"cropVariants="+encodeURIComponent(e),window.open(r,"TYPO3ImageManipulationPreview")}save(t){const e=ImageManipulation.serializeCropVariants(t),r=$(`#${this.trigger.attr("data-field")}`);this.trigger.attr("data-crop-variants",JSON.stringify(t)),this.setPreviewImages(t),r.val(e),FormEngineValidation.markFieldAsChanged(r),this.currentModal.hideModal()}destroy(){this.currentModal&&(this.cropper instanceof Cropper&&this.cropper.destroy(),this.initialized=!1,this.cropper=null,this.currentModal=null,this.data=null)}}export default new ImageManipulation;
\ No newline at end of file
+import $ from"jquery";import{html}from"lit";import{unsafeHTML}from"lit/directives/unsafe-html.js";import"jquery-ui/draggable.js";import"jquery-ui/resizable.js";import FormEngineValidation from"@typo3/backend/form-engine-validation.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Cropper from"cropperjs";import{default as Modal}from"@typo3/backend/modal.js";import"@typo3/backend/element/spinner-element.js";class ImageManipulation{constructor(){this.initialized=!1,this.cropImageContainerSelector="#t3js-crop-image-container",this.cropImageSelector="#t3js-crop-image",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:!0,autoCropArea:.7,dragMode:"crop",guides:!0,responsive:!0,viewMode:1,zoomable:!1,checkCrossOrigin:!1},this.cropBuiltHandler=()=>{this.initialized=!0;const t=this.cropper.getImageData(),e=this.currentModal.querySelector(this.cropImageSelector);$(this.currentModal).find(".cropper-canvas img").removeClass("cropper-hide"),this.imageOriginalSizeFactor=parseInt(e.dataset.originalWidth,10)/t.naturalWidth,this.cropVariantTriggers.each(((e,r)=>{const a=$(r).attr("data-crop-variant-id"),i=this.convertRelativeToAbsoluteCropArea(this.data[a].cropArea,t),o=$.extend(!0,{},this.data[a],{cropArea:i});this.updatePreviewThumbnail(o,$(r))})),this.currentCropVariant.cropArea=this.convertRelativeToAbsoluteCropArea(this.currentCropVariant.cropArea,t),this.cropBox=$(this.currentModal).find(".cropper-crop-box"),this.setCropArea(this.currentCropVariant.cropArea),this.currentCropVariant.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.currentCropVariant.focusArea&&(ImageManipulation.isEmptyArea(this.currentCropVariant.focusArea)&&(this.currentCropVariant.focusArea=$.extend(!0,{},this.defaultFocusArea)),this.initFocusArea(this.cropBox),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)),this.currentCropVariant.selectedRatio&&$(this.currentModal).find(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`).addClass("active")},this.cropMoveHandler=t=>{if(!this.initialized)return;this.currentCropVariant.cropArea=$.extend(!0,this.currentCropVariant.cropArea,{height:Math.floor(t.detail.height),width:Math.floor(t.detail.width),x:Math.floor(t.detail.x),y:Math.floor(t.detail.y)}),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant);const e=Math.round(this.currentCropVariant.cropArea.width*this.imageOriginalSizeFactor),r=Math.round(this.currentCropVariant.cropArea.height*this.imageOriginalSizeFactor);this.cropInfo.text(`${e}×${r} px`)},this.cropStartHandler=()=>{this.currentCropVariant.focusArea&&(this.focusArea.draggable("option","disabled",!0),this.focusArea.resizable("option","disabled",!0))},this.cropEndHandler=()=>{this.currentCropVariant.focusArea&&(this.focusArea.draggable("option","disabled",!1),this.focusArea.resizable("option","disabled",!1))}}static isEmptyArea(t){return $.isEmptyObject(t)}static wait(t,e){window.setTimeout(t,e)}static toCssPercent(t){return 100*t+"%"}static serializeCropVariants(t){return JSON.stringify(t,((t,e)=>"id"===t||"title"===t||"allowedAspectRatios"===t||"coverAreas"===t?void 0:e))}initializeTrigger(){$(".t3js-image-manipulation-trigger").off("click").on("click",(t=>{t.preventDefault(),this.trigger=$(t.currentTarget),this.show()}))}async initializeCropperModal(){const t=this.currentModal.querySelector(this.cropImageSelector);await new Promise((e=>{t.complete?e():t.addEventListener("load",(()=>e()))})),this.init()}show(){const t=this.trigger.data("modalTitle"),e=this.trigger.data("buttonPreviewText"),r=this.trigger.data("buttonDismissText"),a=this.trigger.data("buttonSaveText"),i=this.trigger.data("url"),o=this.trigger.data("payload");this.currentModal=Modal.advanced({additionalCssClasses:["modal-image-manipulation","cropper"],buttons:[{btnClass:"btn-default float-start",name:"preview",icon:"actions-view",text:e},{btnClass:"btn-default",name:"dismiss",icon:"actions-close",text:r},{btnClass:"btn-primary",name:"save",icon:"actions-document-save",text:a}],content:html`<div class="modal-loading"><typo3-backend-spinner size="default"></typo3-backend-spinner></div>`,size:Modal.sizes.full,style:Modal.styles.dark,title:t,staticBackdrop:!0}),this.currentModal.addEventListener("typo3-modal-shown",(()=>{new AjaxRequest(i).post(o).then((async t=>{const e=await t.resolve();this.currentModal.templateResultContent=html`${unsafeHTML(e)}`,this.currentModal.updateComplete.then((()=>this.initializeCropperModal()))}))})),this.currentModal.addEventListener("typo3-modal-hide",(()=>{this.destroy()}))}init(){const t=this.currentModal.querySelector(this.cropImageSelector),e=this.trigger.attr("data-crop-variants");if(!e)throw new TypeError("ImageManipulation: No cropVariants data found for image");this.data=$.isEmptyObject(this.data)?JSON.parse(e):this.data,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("button[name=save]"),this.previewButton=$(this.currentModal).find("button[name=preview]"),this.dismissButton=$(this.currentModal).find("button[name=dismiss]"),this.resetButton=$(this.currentModal).find("button[name=reset]"),this.aspectRatioTrigger=$(this.currentModal).find("label[data-method=setAspectRatio]"),this.currentCropVariant=this.data[this.activeCropVariantTrigger.attr("data-crop-variant-id")],this.cropVariantTriggers.off("click").on("click",(t=>{if($(t.currentTarget).hasClass("is-active"))return t.stopPropagation(),void t.preventDefault();this.activeCropVariantTrigger.removeClass("is-active"),$(t.currentTarget).addClass("is-active"),this.activeCropVariantTrigger=$(t.currentTarget);const e=this.data[this.activeCropVariantTrigger.attr("data-crop-variant-id")],r=this.cropper.getImageData();e.cropArea=this.convertRelativeToAbsoluteCropArea(e.cropArea,r),this.currentCropVariant=$.extend(!0,{},e),this.update(e)})),this.aspectRatioTrigger.off("click").on("click",(t=>{const e=$(t.currentTarget).attr("data-bs-option"),r=$.extend(!0,{},this.currentCropVariant),a=r.allowedAspectRatios[e];this.setAspectRatio(a),this.setCropArea(r.cropArea),this.currentCropVariant=$.extend(!0,{},r,{selectedRatio:e}),this.update(this.currentCropVariant)})),this.saveButton.off("click").on("click",(()=>{this.save(this.data)})),this.trigger.attr("data-preview-url")?this.previewButton.off("click").on("click",(()=>{this.openPreview(this.data)})):this.previewButton.hide(),this.dismissButton.off("click").on("click",(()=>{this.currentModal.hideModal()})),this.resetButton.off("click").on("click",(t=>{const e=this.cropper.getImageData(),r=$(t.currentTarget).attr("data-crop-variant");if(t.preventDefault(),t.stopPropagation(),!r)throw new TypeError("TYPO3 Cropper: No cropVariant data attribute found on reset element.");const a=JSON.parse(r),i=this.convertRelativeToAbsoluteCropArea(a.cropArea,e);this.currentCropVariant=$.extend(!0,{},a,{cropArea:i}),this.update(this.currentCropVariant)})),ImageManipulation.isEmptyArea(this.currentCropVariant.cropArea)&&(this.defaultOpts=$.extend({autoCropArea:1},this.defaultOpts)),this.cropper=new Cropper(t,$.extend(this.defaultOpts,{ready:this.cropBuiltHandler,crop:this.cropMoveHandler,cropend:this.cropEndHandler,cropstart:this.cropStartHandler,data:this.currentCropVariant.cropArea})),this.update(this.currentCropVariant)}update(t){const e=$.extend(!0,{},t),r=t.allowedAspectRatios[t.selectedRatio];$(this.currentModal).find("[data-bs-option]").removeClass("active"),$(this.currentModal).find(`[data-bs-option="${t.selectedRatio}"]`).addClass("active"),this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=$.extend(!0,{},e,t),this.cropBox.find(this.coverAreaSelector).remove(),this.cropBox.has(this.focusAreaSelector).length&&(this.focusArea.resizable("destroy").draggable("destroy"),this.focusArea.remove()),t.focusArea&&(ImageManipulation.isEmptyArea(t.focusArea)&&(this.currentCropVariant.focusArea=$.extend(!0,{},this.defaultFocusArea)),this.initFocusArea(this.cropBox),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)),t.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger)}initFocusArea(t){this.focusArea=$('<div id="t3js-cropper-focus-area" class="cropper-focus-area"></div>'),t.append(this.focusArea),this.focusArea.draggable({containment:t,create:()=>{const t="undefined"!=typeof window&&void 0!==window.document,e=!(!t||!window.document.documentElement)&&"ontouchstart"in window.document.documentElement,r=!!t&&"PointerEvent"in window,a=r?"pointerdown":e?"touchstart":"mousedown",i=r?"pointerup pointercancel":e?"touchend touchcancel":"mouseup";this.focusArea.on(a,(()=>{this.cropper.disable()})),this.focusArea.on(i,(()=>{this.cropper.enable()})),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)},drag:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o,coverAreas:s}=this.currentCropVariant;o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.checkFocusAndCoverAreasCollision(o,s)?this.focusArea.addClass("has-nodrop"):this.focusArea.removeClass("has-nodrop")},revert:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o,coverAreas:s}=this.currentCropVariant;return!!this.checkFocusAndCoverAreasCollision(o,s)&&(this.focusArea.removeClass("has-nodrop"),ImageManipulation.wait((()=>{o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.updateCropVariantData(this.currentCropVariant)}),250),!0)},revertDuration:200,stop:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o}=this.currentCropVariant;o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.scaleAndMoveFocusArea(o)}}).resizable({containment:t,handles:"all",resize:()=>{const{left:e,top:r}=t.offset(),{left:a,top:i}=this.focusArea.offset(),{focusArea:o,coverAreas:s}=this.currentCropVariant;o.height=this.focusArea.height()/t.height(),o.width=this.focusArea.width()/t.width(),o.x=(a-e)/t.width(),o.y=(i-r)/t.height(),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.checkFocusAndCoverAreasCollision(o,s)?this.focusArea.addClass("has-nodrop"):this.focusArea.removeClass("has-nodrop")},stop:(e,r)=>{const{left:a,top:i}=t.offset(),{left:o,top:s}=this.focusArea.offset(),{focusArea:n,coverAreas:c}=this.currentCropVariant;this.checkFocusAndCoverAreasCollision(n,c)?r.element.animate($.extend(r.originalPosition,r.originalSize),250,(()=>{n.height=this.focusArea.height()/t.height(),n.width=this.focusArea.width()/t.width(),n.x=(o-a)/t.width(),n.y=(s-i)/t.height(),this.scaleAndMoveFocusArea(n),this.focusArea.removeClass("has-nodrop")})):this.scaleAndMoveFocusArea(n)}})}initCoverAreas(t,e){e.forEach((e=>{const r=$('<div class="cropper-cover-area t3js-cropper-cover-area"></div>');t.append(r),r.css({height:ImageManipulation.toCssPercent(e.height),left:ImageManipulation.toCssPercent(e.x),top:ImageManipulation.toCssPercent(e.y),width:ImageManipulation.toCssPercent(e.width)})}))}updatePreviewThumbnail(t,e){let r;const a=e.find(".t3js-cropper-preview-thumbnail-crop-area"),i=e.find(".t3js-cropper-preview-thumbnail-crop-image"),o=e.find(".t3js-cropper-preview-thumbnail-focus-area"),s=this.cropper.getImageData();a.css({height:ImageManipulation.toCssPercent(t.cropArea.height/s.naturalHeight),left:ImageManipulation.toCssPercent(t.cropArea.x/s.naturalWidth),top:ImageManipulation.toCssPercent(t.cropArea.y/s.naturalHeight),width:ImageManipulation.toCssPercent(t.cropArea.width/s.naturalWidth)}),t.focusArea&&o.css({height:ImageManipulation.toCssPercent(t.focusArea.height),left:ImageManipulation.toCssPercent(t.focusArea.x),top:ImageManipulation.toCssPercent(t.focusArea.y),width:ImageManipulation.toCssPercent(t.focusArea.width)}),r=a.css(["width","height","left","top"]),i.css({height:parseFloat(r.height)*(1/(t.cropArea.height/s.naturalHeight))+"px",margin:-1*parseFloat(r.left)+"px",marginTop:-1*parseFloat(r.top)+"px",width:parseFloat(r.width)*(1/(t.cropArea.width/s.naturalWidth))+"px"})}scaleAndMoveFocusArea(t){this.focusArea.css({height:ImageManipulation.toCssPercent(t.height),left:ImageManipulation.toCssPercent(t.x),top:ImageManipulation.toCssPercent(t.y),width:ImageManipulation.toCssPercent(t.width)}),this.currentCropVariant.focusArea=t,this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant)}updateCropVariantData(t){const e=this.cropper.getImageData(),r=this.convertAbsoluteToRelativeCropArea(t.cropArea,e);this.data[t.id]=$.extend(!0,{},t,{cropArea:r})}setAspectRatio(t){this.cropper.setAspectRatio(t.value)}setCropArea(t){const e=this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];0===e.value?this.cropper.setData({height:t.height,width:t.width,x:t.x,y:t.y}):this.cropper.setData({height:t.height,width:t.height*e.value,x:t.x,y:t.y})}checkFocusAndCoverAreasCollision(t,e){return!!e&&e.some((e=>t.x<e.x+e.width&&t.x+t.width>e.x&&t.y<e.y+e.height&&t.height+t.y>e.y))}convertAbsoluteToRelativeCropArea(t,e){const{height:r,width:a,x:i,y:o}=t;return{height:r/e.naturalHeight,width:a/e.naturalWidth,x:i/e.naturalWidth,y:o/e.naturalHeight}}convertRelativeToAbsoluteCropArea(t,e){const{height:r,width:a,x:i,y:o}=t;return{height:r*e.naturalHeight,width:a*e.naturalWidth,x:i*e.naturalWidth,y:o*e.naturalHeight}}setPreviewImages(t){const e=this.cropper.image,r=this.cropper.getImageData();Object.keys(t).forEach((a=>{const i=t[a],o=this.convertRelativeToAbsoluteCropArea(i.cropArea,r),s=this.trigger.closest(".form-group").find(`.t3js-image-manipulation-preview[data-crop-variant-id="${a}"]`),n=this.trigger.closest(".form-group").find(`.t3js-image-manipulation-selected-ratio[data-crop-variant-id="${a}"]`);if(0===s.length)return;let c=s.width(),h=s.data("preview-height");const p=o.width/o.height,d=c/p;d>h?c=h*p:h=d,c>o.width&&(c=o.width,h=o.height);const l=c/o.width,u=$("<div />").html('<img src="'+e.src+'">'),g=$(this.currentModal).find(`.t3-js-ratio-title[data-ratio-id="${i.id}${i.selectedRatio}"]`);n.text(g.text()),u.addClass("cropper-preview-container"),s.empty().append(u),u.wrap('<span class="thumbnail thumbnail-status"></span>'),u.width(c).height(h).find("img").css({height:r.naturalHeight*l,left:-o.x*l,top:-o.y*l,width:r.naturalWidth*l})}))}openPreview(t){const e=ImageManipulation.serializeCropVariants(t);let r=this.trigger.attr("data-preview-url");r=r+(r.includes("?")?"&":"?")+"cropVariants="+encodeURIComponent(e),window.open(r,"TYPO3ImageManipulationPreview")}save(t){const e=ImageManipulation.serializeCropVariants(t),r=$(`#${this.trigger.attr("data-field")}`);this.trigger.attr("data-crop-variants",JSON.stringify(t)),this.setPreviewImages(t),r.val(e),FormEngineValidation.markFieldAsChanged(r),this.currentModal.hideModal()}destroy(){this.currentModal&&(this.cropper instanceof Cropper&&this.cropper.destroy(),this.initialized=!1,this.cropper=null,this.currentModal=null,this.data=null)}}export default new ImageManipulation;
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/modal.js b/typo3/sysext/backend/Resources/Public/JavaScript/modal.js
index a76805ffdc98..333da374f71f 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/modal.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/modal.js
@@ -10,11 +10,12 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-var Identifiers,__decorate=function(t,e,a,o){var s,n=arguments.length,i=n<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,a):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(t,e,a,o);else for(var l=t.length-1;l>=0;l--)(s=t[l])&&(i=(n<3?s(i):n>3?s(e,a,i):s(e,a))||i);return n>3&&i&&Object.defineProperty(e,a,i),i};import{Modal as BootstrapModal}from"bootstrap";import{html,nothing,LitElement}from"lit";import{customElement,property,state}from"lit/decorators.js";import{unsafeHTML}from"lit/directives/unsafe-html.js";import{classMap}from"lit/directives/class-map.js";import{styleMap}from"lit/directives/style-map.js";import{ifDefined}from"lit/directives/if-defined.js";import{classesArrayToClassInfo}from"@typo3/core/lit-helper.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Severity from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/element/spinner-element.js";!function(t){t.modal=".t3js-modal",t.content=".t3js-modal-content",t.close=".t3js-modal-close",t.body=".t3js-modal-body",t.footer=".t3js-modal-footer"}(Identifiers||(Identifiers={}));export var Sizes;!function(t){t.small="small",t.default="default",t.medium="medium",t.large="large",t.full="full"}(Sizes||(Sizes={}));export var Styles;!function(t){t.default="default",t.light="light",t.dark="dark"}(Styles||(Styles={}));export var Types;!function(t){t.default="default",t.template="template",t.ajax="ajax",t.iframe="iframe"}(Types||(Types={}));let ModalElement=class extends LitElement{constructor(){super(...arguments),this.modalTitle="",this.content="",this.type=Types.default,this.severity=SeverityEnum.notice,this.variant=Styles.default,this.size=Sizes.default,this.zindex=5e3,this.additionalCssClasses=[],this.buttons=[],this.templateResultContent=null,this.activeButton=null,this.bootstrapModal=null,this.callback=null,this.ajaxCallback=null,this.userData={}}hideModal(){this.bootstrapModal&&this.bootstrapModal.hide()}createRenderRoot(){return this}firstUpdated(){this.bootstrapModal=new BootstrapModal(this.renderRoot.querySelector(Identifiers.modal),{}),this.bootstrapModal.show(),this.callback&&this.callback(this)}render(){const t={zIndex:this.zindex.toString()},e=classesArrayToClassInfo([`modal-type-${this.type}`,`modal-severity-${Severity.getCssClass(this.severity)}`,`modal-style-${this.variant}`,`modal-size-${this.size}`,...this.additionalCssClasses]);return html`
+var Identifiers,__decorate=function(t,e,a,o){var s,n=arguments.length,i=n<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,a):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(t,e,a,o);else for(var l=t.length-1;l>=0;l--)(s=t[l])&&(i=(n<3?s(i):n>3?s(e,a,i):s(e,a))||i);return n>3&&i&&Object.defineProperty(e,a,i),i};import{Modal as BootstrapModal}from"bootstrap";import{html,nothing,LitElement}from"lit";import{customElement,property,state}from"lit/decorators.js";import{unsafeHTML}from"lit/directives/unsafe-html.js";import{classMap}from"lit/directives/class-map.js";import{styleMap}from"lit/directives/style-map.js";import{ifDefined}from"lit/directives/if-defined.js";import{classesArrayToClassInfo}from"@typo3/core/lit-helper.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Severity from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/element/spinner-element.js";!function(t){t.modal=".t3js-modal",t.content=".t3js-modal-content",t.close=".t3js-modal-close",t.body=".t3js-modal-body",t.footer=".t3js-modal-footer"}(Identifiers||(Identifiers={}));export var Sizes;!function(t){t.small="small",t.default="default",t.medium="medium",t.large="large",t.full="full"}(Sizes||(Sizes={}));export var Styles;!function(t){t.default="default",t.light="light",t.dark="dark"}(Styles||(Styles={}));export var Types;!function(t){t.default="default",t.template="template",t.ajax="ajax",t.iframe="iframe"}(Types||(Types={}));let ModalElement=class extends LitElement{constructor(){super(...arguments),this.modalTitle="",this.content="",this.type=Types.default,this.severity=SeverityEnum.notice,this.variant=Styles.default,this.size=Sizes.default,this.zindex=5e3,this.staticBackdrop=!1,this.additionalCssClasses=[],this.buttons=[],this.templateResultContent=null,this.activeButton=null,this.bootstrapModal=null,this.callback=null,this.ajaxCallback=null,this.userData={}}hideModal(){this.bootstrapModal&&this.bootstrapModal.hide()}createRenderRoot(){return this}firstUpdated(){this.bootstrapModal=new BootstrapModal(this.renderRoot.querySelector(Identifiers.modal),{}),this.bootstrapModal.show(),this.callback&&this.callback(this)}render(){const t={zIndex:this.zindex.toString()},e=classesArrayToClassInfo([`modal-type-${this.type}`,`modal-severity-${Severity.getCssClass(this.severity)}`,`modal-style-${this.variant}`,`modal-size-${this.size}`,...this.additionalCssClasses]);return html`
       <div
           tabindex="-1"
           class="modal fade t3js-modal ${classMap(e)}"
           style=${styleMap(t)}
+          data-bs-backdrop="${!ifDefined(this.staticBackdrop)||"static"}"
           @show.bs.modal=${()=>this.trigger("typo3-modal-show")}
           @shown.bs.modal=${()=>this.trigger("typo3-modal-shown")}
           @hide.bs.modal=${()=>this.trigger("typo3-modal-hide")}
@@ -53,4 +54,4 @@ var Identifiers,__decorate=function(t,e,a,o){var s,n=arguments.length,i=n<3?e:nu
           ${t.icon?html`<typo3-backend-icon identifier="${t.icon}" size="small"></typo3-backend-icon>`:nothing}
           ${t.text}
       </button>
-    `}trigger(t){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0}))}};__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"modalTitle",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"content",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"type",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"severity",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"variant",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"size",void 0),__decorate([property({type:Number,reflect:!0})],ModalElement.prototype,"zindex",void 0),__decorate([property({type:Array})],ModalElement.prototype,"additionalCssClasses",void 0),__decorate([property({type:Array,attribute:!1})],ModalElement.prototype,"buttons",void 0),__decorate([state()],ModalElement.prototype,"templateResultContent",void 0),__decorate([state()],ModalElement.prototype,"activeButton",void 0),ModalElement=__decorate([customElement("typo3-backend-modal")],ModalElement);export{ModalElement};class Modal{constructor(){this.sizes=Sizes,this.styles=Styles,this.types=Types,this.currentModal=null,this.instances=[],this.defaultConfiguration={type:Types.default,title:"Information",content:"No content provided, please check your <code>Modal</code> configuration.",severity:SeverityEnum.notice,buttons:[],style:Styles.default,size:Sizes.default,additionalCssClasses:[],callback:null,ajaxCallback:null},this.initializeMarkupTrigger(document)}static createModalResponseEventFromElement(t,e){return t.dataset.eventName?new CustomEvent(t.dataset.eventName,{bubbles:!0,detail:{result:e,payload:t.dataset.eventPayload||null}}):null}dismiss(){this.currentModal&&this.currentModal.hideModal()}confirm(t,e,a=SeverityEnum.warning,o=[],s){0===o.length&&o.push({text:TYPO3.lang["button.cancel"]||"Cancel",active:!0,btnClass:"btn-default",name:"cancel"},{text:TYPO3.lang["button.ok"]||"OK",btnClass:"btn-"+Severity.getCssClass(a),name:"ok"});const n=this.advanced({title:t,content:e,severity:a,buttons:o,additionalCssClasses:s});return n.addEventListener("button.clicked",(t=>{const e=t.target;"cancel"===e.getAttribute("name")?e.dispatchEvent(new CustomEvent("confirm.button.cancel",{bubbles:!0})):"ok"===e.getAttribute("name")&&e.dispatchEvent(new CustomEvent("confirm.button.ok",{bubbles:!0}))})),n}loadUrl(t,e=SeverityEnum.info,a,o,s){return this.advanced({type:Types.ajax,title:t,severity:e,buttons:a,ajaxCallback:s,content:o})}show(t,e,a=SeverityEnum.info,o,s){return this.advanced({type:Types.default,title:t,content:e,severity:a,buttons:o,additionalCssClasses:s})}advanced(t){return t.type="string"==typeof t.type&&t.type in Types?t.type:this.defaultConfiguration.type,t.title="string"==typeof t.title?t.title:this.defaultConfiguration.title,t.content="string"==typeof t.content||"object"==typeof t.content?t.content:this.defaultConfiguration.content,t.severity=void 0!==t.severity?t.severity:this.defaultConfiguration.severity,t.buttons=t.buttons||this.defaultConfiguration.buttons,t.size="string"==typeof t.size&&t.size in Sizes?t.size:this.defaultConfiguration.size,t.style="string"==typeof t.style&&t.style in Styles?t.style:this.defaultConfiguration.style,t.additionalCssClasses=t.additionalCssClasses||this.defaultConfiguration.additionalCssClasses,t.callback="function"==typeof t.callback?t.callback:this.defaultConfiguration.callback,t.ajaxCallback="function"==typeof t.ajaxCallback?t.ajaxCallback:this.defaultConfiguration.ajaxCallback,this.generate(t)}setButtons(t){return this.currentModal.buttons=t,this.currentModal}initializeMarkupTrigger(t){new RegularEvent("click",((t,e)=>{t.preventDefault();const a=e.dataset.bsContent||"Are you sure?";let o=SeverityEnum.info;if(e.dataset.severity in SeverityEnum){const t=e.dataset.severity;o=SeverityEnum[t]}let s=e.dataset.url||null;if(null!==s){const t=s.includes("?")?"&":"?";s=s+t+new URLSearchParams(e.dataset).toString()}this.advanced({type:null!==s?Types.ajax:Types.default,title:e.dataset.title||"Alert",content:null!==s?s:a,severity:o,buttons:[{text:e.dataset.buttonCloseText||TYPO3.lang["button.close"]||"Close",active:!0,btnClass:"btn-default",trigger:(t,a)=>{a.hideModal();const o=Modal.createModalResponseEventFromElement(e,!1);null!==o&&e.dispatchEvent(o)}},{text:e.dataset.buttonOkText||TYPO3.lang["button.ok"]||"OK",btnClass:"btn-"+Severity.getCssClass(o),trigger:(t,a)=>{a.hideModal();const o=Modal.createModalResponseEventFromElement(e,!0);null!==o&&e.dispatchEvent(o);let s=e.dataset.uri||e.dataset.href||e.getAttribute("href");s&&"#"!==s&&(e.ownerDocument.location.href=s),"submit"===e.getAttribute("type")&&(e.closest("form")?.submit(),"BUTTON"===e.tagName&&e.hasAttribute("form")&&e.ownerDocument.querySelector("form#"+e.getAttribute("form"))?.submit()),e.dataset.targetForm&&e.ownerDocument.querySelector("form#"+e.dataset.targetForm)?.submit()}}]})})).delegateTo(t,".t3js-modal-trigger")}generate(t){const e=document.createElement("typo3-backend-modal");return e.type=t.type,"string"==typeof t.content?e.content=t.content:t.type===Types.default&&(e.type=Types.template,e.templateResultContent=t.content),e.severity=t.severity,e.variant=t.style,e.size=t.size,e.modalTitle=t.title,e.additionalCssClasses=t.additionalCssClasses,e.buttons=t.buttons,t.callback&&(e.callback=t.callback),t.ajaxCallback&&(e.ajaxCallback=t.ajaxCallback),e.addEventListener("typo3-modal-shown",(()=>{const t=e.nextElementSibling,a=1e3+10*this.instances.length;e.zindex=a;const o=a-5;t.style.zIndex=o.toString(),e.querySelector(`${Identifiers.footer} .t3js-active`)?.focus()})),e.addEventListener("typo3-modal-hide",(()=>{if(this.instances.length>0){const t=this.instances.length-1;this.instances.splice(t,1),this.currentModal=this.instances[t-1]}})),e.addEventListener("typo3-modal-hidden",(t=>{e.remove(),this.instances.length>0&&document.body.classList.add("modal-open")})),e.addEventListener("typo3-modal-show",(()=>{this.currentModal=e,this.instances.push(e)})),document.body.appendChild(e),e}}let modalObject=null;try{parent&&parent.window.TYPO3&&parent.window.TYPO3.Modal?(parent.window.TYPO3.Modal.initializeMarkupTrigger(document),modalObject=parent.window.TYPO3.Modal):top&&top.TYPO3.Modal&&(top.TYPO3.Modal.initializeMarkupTrigger(document),modalObject=top.TYPO3.Modal)}catch{}modalObject||(modalObject=new Modal,TYPO3.Modal=modalObject);export default modalObject;
\ No newline at end of file
+    `}trigger(t){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0}))}};__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"modalTitle",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"content",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"type",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"severity",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"variant",void 0),__decorate([property({type:String,reflect:!0})],ModalElement.prototype,"size",void 0),__decorate([property({type:Number,reflect:!0})],ModalElement.prototype,"zindex",void 0),__decorate([property({type:Boolean})],ModalElement.prototype,"staticBackdrop",void 0),__decorate([property({type:Array})],ModalElement.prototype,"additionalCssClasses",void 0),__decorate([property({type:Array,attribute:!1})],ModalElement.prototype,"buttons",void 0),__decorate([state()],ModalElement.prototype,"templateResultContent",void 0),__decorate([state()],ModalElement.prototype,"activeButton",void 0),ModalElement=__decorate([customElement("typo3-backend-modal")],ModalElement);export{ModalElement};class Modal{constructor(){this.sizes=Sizes,this.styles=Styles,this.types=Types,this.currentModal=null,this.instances=[],this.defaultConfiguration={type:Types.default,title:"Information",content:"No content provided, please check your <code>Modal</code> configuration.",severity:SeverityEnum.notice,buttons:[],style:Styles.default,size:Sizes.default,additionalCssClasses:[],callback:null,ajaxCallback:null,staticBackdrop:!1},this.initializeMarkupTrigger(document)}static createModalResponseEventFromElement(t,e){return t.dataset.eventName?new CustomEvent(t.dataset.eventName,{bubbles:!0,detail:{result:e,payload:t.dataset.eventPayload||null}}):null}dismiss(){this.currentModal&&this.currentModal.hideModal()}confirm(t,e,a=SeverityEnum.warning,o=[],s){0===o.length&&o.push({text:TYPO3.lang["button.cancel"]||"Cancel",active:!0,btnClass:"btn-default",name:"cancel"},{text:TYPO3.lang["button.ok"]||"OK",btnClass:"btn-"+Severity.getCssClass(a),name:"ok"});const n=this.advanced({title:t,content:e,severity:a,buttons:o,additionalCssClasses:s});return n.addEventListener("button.clicked",(t=>{const e=t.target;"cancel"===e.getAttribute("name")?e.dispatchEvent(new CustomEvent("confirm.button.cancel",{bubbles:!0})):"ok"===e.getAttribute("name")&&e.dispatchEvent(new CustomEvent("confirm.button.ok",{bubbles:!0}))})),n}loadUrl(t,e=SeverityEnum.info,a,o,s){return this.advanced({type:Types.ajax,title:t,severity:e,buttons:a,ajaxCallback:s,content:o})}show(t,e,a=SeverityEnum.info,o,s){return this.advanced({type:Types.default,title:t,content:e,severity:a,buttons:o,additionalCssClasses:s})}advanced(t){return t.type="string"==typeof t.type&&t.type in Types?t.type:this.defaultConfiguration.type,t.title="string"==typeof t.title?t.title:this.defaultConfiguration.title,t.content="string"==typeof t.content||"object"==typeof t.content?t.content:this.defaultConfiguration.content,t.severity=void 0!==t.severity?t.severity:this.defaultConfiguration.severity,t.buttons=t.buttons||this.defaultConfiguration.buttons,t.size="string"==typeof t.size&&t.size in Sizes?t.size:this.defaultConfiguration.size,t.style="string"==typeof t.style&&t.style in Styles?t.style:this.defaultConfiguration.style,t.additionalCssClasses=t.additionalCssClasses||this.defaultConfiguration.additionalCssClasses,t.callback="function"==typeof t.callback?t.callback:this.defaultConfiguration.callback,t.ajaxCallback="function"==typeof t.ajaxCallback?t.ajaxCallback:this.defaultConfiguration.ajaxCallback,t.staticBackdrop=t.staticBackdrop||this.defaultConfiguration.staticBackdrop,this.generate(t)}setButtons(t){return this.currentModal.buttons=t,this.currentModal}initializeMarkupTrigger(t){new RegularEvent("click",((t,e)=>{t.preventDefault();const a=e.dataset.bsContent||"Are you sure?";let o=SeverityEnum.info;if(e.dataset.severity in SeverityEnum){const t=e.dataset.severity;o=SeverityEnum[t]}let s=e.dataset.url||null;if(null!==s){const t=s.includes("?")?"&":"?";s=s+t+new URLSearchParams(e.dataset).toString()}this.advanced({type:null!==s?Types.ajax:Types.default,title:e.dataset.title||"Alert",content:null!==s?s:a,severity:o,staticBackdrop:void 0!==e.dataset.staticBackdrop,buttons:[{text:e.dataset.buttonCloseText||TYPO3.lang["button.close"]||"Close",active:!0,btnClass:"btn-default",trigger:(t,a)=>{a.hideModal();const o=Modal.createModalResponseEventFromElement(e,!1);null!==o&&e.dispatchEvent(o)}},{text:e.dataset.buttonOkText||TYPO3.lang["button.ok"]||"OK",btnClass:"btn-"+Severity.getCssClass(o),trigger:(t,a)=>{a.hideModal();const o=Modal.createModalResponseEventFromElement(e,!0);null!==o&&e.dispatchEvent(o);let s=e.dataset.uri||e.dataset.href||e.getAttribute("href");s&&"#"!==s&&(e.ownerDocument.location.href=s),"submit"===e.getAttribute("type")&&(e.closest("form")?.submit(),"BUTTON"===e.tagName&&e.hasAttribute("form")&&e.ownerDocument.querySelector("form#"+e.getAttribute("form"))?.submit()),e.dataset.targetForm&&e.ownerDocument.querySelector("form#"+e.dataset.targetForm)?.submit()}}]})})).delegateTo(t,".t3js-modal-trigger")}generate(t){const e=document.createElement("typo3-backend-modal");return e.type=t.type,"string"==typeof t.content?e.content=t.content:t.type===Types.default&&(e.type=Types.template,e.templateResultContent=t.content),e.severity=t.severity,e.variant=t.style,e.size=t.size,e.modalTitle=t.title,e.additionalCssClasses=t.additionalCssClasses,e.buttons=t.buttons,e.staticBackdrop=t.staticBackdrop,t.callback&&(e.callback=t.callback),t.ajaxCallback&&(e.ajaxCallback=t.ajaxCallback),e.addEventListener("typo3-modal-shown",(()=>{const t=e.nextElementSibling,a=1e3+10*this.instances.length;e.zindex=a;const o=a-5;t.style.zIndex=o.toString(),e.querySelector(`${Identifiers.footer} .t3js-active`)?.focus()})),e.addEventListener("typo3-modal-hide",(()=>{if(this.instances.length>0){const t=this.instances.length-1;this.instances.splice(t,1),this.currentModal=this.instances[t-1]}})),e.addEventListener("typo3-modal-hidden",(t=>{e.remove(),this.instances.length>0&&document.body.classList.add("modal-open")})),e.addEventListener("typo3-modal-show",(()=>{this.currentModal=e,this.instances.push(e)})),document.body.appendChild(e),e}}let modalObject=null;try{parent&&parent.window.TYPO3&&parent.window.TYPO3.Modal?(parent.window.TYPO3.Modal.initializeMarkupTrigger(document),modalObject=parent.window.TYPO3.Modal):top&&top.TYPO3.Modal&&(top.TYPO3.Modal.initializeMarkupTrigger(document),modalObject=top.TYPO3.Modal)}catch{}modalObject||(modalObject=new Modal,TYPO3.Modal=modalObject);export default modalObject;
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99092-AllowStaticBackdropsInModals.rst b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99092-AllowStaticBackdropsInModals.rst
new file mode 100644
index 000000000000..83c0ca9b07c8
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99092-AllowStaticBackdropsInModals.rst
@@ -0,0 +1,53 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99092-1668509154:
+
+==================================================
+Feature: #99092 - Allow static backdrops in modals
+==================================================
+
+See :issue:`99092`
+
+Description
+===========
+
+The Modal API is now able to render a static backdrop to avoid closing the modal
+when clicking it which may come handy in case closing the modal would result in
+a negative user experience, e.g. in the image cropper.
+
+
+Impact
+======
+
+The new boolean configuration option :js:`staticBackdrop` controls whether a
+static backdrop should be rendered or not, defaulting to :js:`false`.
+
+Example:
+
+.. code-block:: js
+
+    import {default as Modal} from './modal';
+
+    Modal.advanced(
+      title: 'Hello',
+      content: 'This modal is not closable via clicking the backdrop.',
+      size: Modal.sizes.small,
+      staticBackdrop: true
+    );
+
+Templates, using the HTML class :html:`.t3js-modal-trigger` to initialize
+a modal dialog are also able to use the new option by adding the
+:html:`data-static-backdrop` attribute to the corresponding element.
+
+Example:
+
+.. code-block:: html
+
+    <button class="btn btn-default t3js-modal-trigger"
+       data-title="Hello"
+       data-bs-content="This modal is not closable via clicking the backdrop."
+       data-static-backdrop>
+        Open modal
+    </button>
+
+.. index:: Backend, JavaScript, ext:backend
-- 
GitLab