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