From beff4cb19c70b10bbe1d3084c0d50c21cfc6b055 Mon Sep 17 00:00:00 2001 From: Willi Wehmeier <wwwehmeier@gmail.com> Date: Mon, 29 Apr 2024 10:22:39 +0200 Subject: [PATCH] [TASK] Make cropper presets usable with keyboard The aspect ratio buttons in the image cropping tool have a visible focus state now to make them usable when navigating with the keyboard tab key. Additionally the button can now be pressed with the space and return key. Resolves: #103760 Releases: main, 12.4 Change-Id: Id221daddeee9d5b61497cb74604dbf151d25e3ee Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84079 Tested-by: Andreas Kienast <a.fernandez@scripting-base.de> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Michael Telgkamp <michael.telgkamp@mindscreen.de> Tested-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Andreas Kienast <a.fernandez@scripting-base.de> Tested-by: Michael Telgkamp <michael.telgkamp@mindscreen.de> Reviewed-by: Georg Ringer <georg.ringer@gmail.com> --- .../Sources/Sass/typo3/_element_cropper.scss | 6 ++++ .../TypeScript/backend/image-manipulation.ts | 33 +++++++++++++++---- .../backend/Resources/Public/Css/backend.css | 1 + .../Public/JavaScript/image-manipulation.js | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Build/Sources/Sass/typo3/_element_cropper.scss b/Build/Sources/Sass/typo3/_element_cropper.scss index f0c1ab61c20d..5dce84f5a7e1 100644 --- a/Build/Sources/Sass/typo3/_element_cropper.scss +++ b/Build/Sources/Sass/typo3/_element_cropper.scss @@ -105,6 +105,12 @@ table, label { color: $color-white; + + &:has(input[type="radio"]:focus) { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); + } } position: relative; margin: -15px; diff --git a/Build/Sources/TypeScript/backend/image-manipulation.ts b/Build/Sources/TypeScript/backend/image-manipulation.ts index 66e7d9548886..873e02d5d84e 100644 --- a/Build/Sources/TypeScript/backend/image-manipulation.ts +++ b/Build/Sources/TypeScript/backend/image-manipulation.ts @@ -304,16 +304,24 @@ class ImageManipulation { 'click', (evt: Event, target: HTMLElement): void => { const ratioId: string = target.dataset.bsOption; - const temp: CropVariant = Object.assign({}, this.currentCropVariant); - const ratio: Ratio = temp.allowedAspectRatios[ratioId]; - this.setAspectRatio(ratio); - // set data explicitly or setAspectRatio upscales the crop - this.setCropArea(temp.cropArea); - this.currentCropVariant = Object.assign({}, temp, { selectedRatio: ratioId }); - this.update(this.currentCropVariant); + this.handleAspectRatioChange(ratioId); } ).delegateTo(this.currentModal, 'label[data-method=setAspectRatio]'); + new RegularEvent('keydown', (evt: KeyboardEvent, target: HTMLElement): void => { + if (!['Enter', 'Space'].includes(evt.code)) { + return; + } + evt.preventDefault(); + evt.stopImmediatePropagation(); + + const setAspectRatioLabel = target.closest('label[data-method="setAspectRatio"]') as HTMLElement; + const ratioId: string = setAspectRatioLabel.dataset.bsOption; + + setAspectRatioLabel.querySelector('input').checked = true; + this.handleAspectRatioChange(ratioId); + }).delegateTo(this.currentModal, 'label[data-method="setAspectRatio"] input[type="radio"]'); + /** * Assign EventListener to saveButton */ @@ -475,6 +483,17 @@ class ImageManipulation { this.cropInfo.innerText = `${naturalWidth}×${naturalHeight} px`; }; + private handleAspectRatioChange(ratioId: string): void { + const temp: CropVariant = Object.assign({}, this.currentCropVariant); + const ratio: Ratio = temp.allowedAspectRatios[ratioId]; + this.setAspectRatio(ratio); + // set data explicitly or setAspectRatio upscales the crop + this.setCropArea(temp.cropArea); + this.currentCropVariant = Object.assign({}, temp, { selectedRatio: ratioId }); + + this.update(this.currentCropVariant); + } + /** * @desc Update current cropArea position and size when changing cropVariants * @param {CropVariant} cropVariant - The new cropVariant to update the UI with diff --git a/typo3/sysext/backend/Resources/Public/Css/backend.css b/typo3/sysext/backend/Resources/Public/Css/backend.css index 00da5dacaf52..1a52235033df 100644 --- a/typo3/sysext/backend/Resources/Public/Css/backend.css +++ b/typo3/sysext/backend/Resources/Public/Css/backend.css @@ -3924,6 +3924,7 @@ typo3-rte-ckeditor-ckeditor5 .ck.ck-style-panel .ck-style-grid .ck-style-grid__b .cropper .panel-group [aria-expanded=true][data-bs-toggle=collapse]{background-color:#333} .cropper .panel-group [aria-expanded=false]{border-inline-start:2px solid #444;position:relative} .cropper .panel-group label,.cropper .panel-group table{color:#fff} +.cropper .panel-group label:has(input[type=radio]:focus),.cropper .panel-group table:has(input[type=radio]:focus){color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)} .cropper .panel-collapse.collapse{background-color:#2c2c2c!important} .cropper .panel-heading{padding:0} .cropper .panel-heading .panel-title>[data-crop-variant]{display:flex;padding:10px 15px;justify-content:space-between} diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js b/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js index be39672b1110..dab93b2470d8 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/image-manipulation.js @@ -10,7 +10,7 @@ * * The TYPO3 project - inspiring people to share! */ -import{html}from"lit";import{unsafeHTML}from"lit/directives/unsafe-html.js";import{styleMap}from"lit/directives/style-map.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import RegularEvent from"@typo3/core/event/regular-event.js";import FormEngineValidation from"@typo3/backend/form-engine-validation.js";import Cropper from"cropperjs";import{default as Modal}from"@typo3/backend/modal.js";import"@typo3/backend/element/spinner-element.js";import{renderNodes}from"@typo3/core/lit-helper.js";import{Offset}from"@typo3/backend/element/draggable-resizable-element.js";class ImageManipulation{constructor(){this.initialized=!1,this.triggerListener=null,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.querySelector(".cropper-canvas img")?.classList.remove("cropper-hide"),this.imageOriginalSizeFactor=parseInt(e.dataset.originalWidth,10)/t.naturalWidth,this.cropVariantTriggers.forEach((e=>{const r=e.dataset.cropVariantId,a=this.convertRelativeToAbsoluteCropArea(this.data[r].cropArea,t),i=Object.assign({},this.data[r],{cropArea:a});this.updatePreviewThumbnail(i,e)})),this.currentCropVariant.cropArea=this.convertRelativeToAbsoluteCropArea(this.currentCropVariant.cropArea,t),this.cropBox=this.currentModal.querySelector(".cropper-crop-box"),this.setCropArea(this.currentCropVariant.cropArea),this.currentCropVariant.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.currentCropVariant.focusArea&&(ImageManipulation.isEmptyObject(this.currentCropVariant.focusArea)&&(this.currentCropVariant.focusArea=Object.assign({},this.defaultFocusArea)),this.focusAreaEl?.remove(),this.initFocusArea(this.cropBox)),this.currentCropVariant.selectedRatio&&this.currentModal.querySelector(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`)?.classList.add("active")},this.cropMoveHandler=t=>{if(!this.initialized)return;let e=Math.floor(t.detail.width),r=Math.floor(t.detail.height);(e<15||r<15)&&(e=Math.max(15,r),r=Math.max(15,e),this.cropper.setData({width:e,height:r})),this.currentCropVariant.cropArea=Object.assign({},this.currentCropVariant.cropArea,{width:Math.floor(e),height:Math.floor(r),x:Math.floor(t.detail.x),y:Math.floor(t.detail.y)}),this.focusAreaEl&&this.currentCropVariant?.focusArea&&(this.focusAreaEl.offset=this.convertAreaToOffset(this.currentCropVariant.focusArea,this.cropBox)),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant);const a=Math.round(this.currentCropVariant.cropArea.width*this.imageOriginalSizeFactor),i=Math.round(this.currentCropVariant.cropArea.height*this.imageOriginalSizeFactor);this.cropInfo.innerText=`${a}×${i} px`}}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))}static isEmptyObject(t){return!t||"object"!=typeof t||0===Object.keys(t).length||"{}"===JSON.stringify(t)}static resolvePointerEventNames(){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=e?["touchmove"]:["mousemove"],i=e?["touchstart"]:["mousedown"],o=e?["touchend","touchcancel"]:["mouseup"];return{touchStart:i,touchMove:a,touchEnd:o,pointerDown:r?["pointerdown"]:i,pointerMove:r?["pointermove"]:a,pointerUp:r?["pointerup","pointercancel"]:o}}initializeTrigger(){this.triggerListener||(this.triggerListener=new RegularEvent("click",((t,e)=>{t.preventDefault(),this.trigger=e,this.show()})),this.triggerListener.delegateTo(document,".t3js-image-manipulation-trigger"))}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.dataset,e=t.modalTitle,r=t.buttonPreviewText,a=t.buttonDismissText,i=t.buttonSaveText,o=t.url,s=JSON.parse(t.payload);this.currentModal=Modal.advanced({additionalCssClasses:["modal-image-manipulation","cropper"],buttons:[{btnClass:"btn-default float-start",name:"preview",icon:"actions-view",text:r},{btnClass:"btn-default",name:"dismiss",icon:"actions-close",text:a},{btnClass:"btn-primary",name:"save",icon:"actions-document-save",text:i}],content:html`<div class="modal-loading"><typo3-backend-spinner size="large"></typo3-backend-spinner></div>`,size:Modal.sizes.full,style:Modal.styles.dark,title:e,staticBackdrop:!0}),this.currentModal.addEventListener("typo3-modal-shown",(()=>{new AjaxRequest(o).post(s).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.dataset.cropVariants;if(!e)throw new TypeError("ImageManipulation: No cropVariants data found for image");this.data=ImageManipulation.isEmptyObject(this.data)?JSON.parse(e):this.data,this.cropVariantTriggers=this.currentModal.querySelectorAll(".t3js-crop-variant-trigger"),this.activeCropVariantTrigger=this.currentModal.querySelector(".t3js-crop-variant-trigger.is-active"),this.cropInfo=this.currentModal.querySelector(this.cropInfoSelector),this.currentCropVariant=this.data[this.activeCropVariantTrigger.dataset.cropVariantId],this.cropVariantTriggers.forEach((t=>t.addEventListener("click",(t=>{if(t.currentTarget.classList.contains("is-active"))return t.stopPropagation(),void t.preventDefault();this.activeCropVariantTrigger.classList.remove("is-active"),t.currentTarget.classList.add("is-active"),this.activeCropVariantTrigger=t.currentTarget;const e=this.data[this.activeCropVariantTrigger.dataset.cropVariantId],r=this.cropper.getImageData();e.cropArea=this.convertRelativeToAbsoluteCropArea(e.cropArea,r),this.currentCropVariant=Object.assign({},e),this.update(e)})))),new RegularEvent("click",((t,e)=>{const r=e.dataset.bsOption,a=Object.assign({},this.currentCropVariant),i=a.allowedAspectRatios[r];this.setAspectRatio(i),this.setCropArea(a.cropArea),this.currentCropVariant=Object.assign({},a,{selectedRatio:r}),this.update(this.currentCropVariant)})).delegateTo(this.currentModal,"label[data-method=setAspectRatio]"),new RegularEvent("click",(()=>this.save(this.data))).delegateTo(this.currentModal,"button[name=save]"),this.trigger.dataset.previewUrl?new RegularEvent("click",(()=>this.openPreview(this.data))).delegateTo(this.currentModal,"button[name=preview]"):this.currentModal.querySelectorAll("button[name=preview]").forEach((t=>t.style.display="none")),new RegularEvent("click",(()=>this.currentModal.hideModal())).delegateTo(this.currentModal,"button[name=dismiss]"),new RegularEvent("click",((t,e)=>{const r=this.cropper.getImageData(),a=e.dataset.cropVariant;if(t.preventDefault(),t.stopPropagation(),!a)throw new TypeError("TYPO3 Cropper: No cropVariant data attribute found on reset element.");const i=JSON.parse(a),o=this.convertRelativeToAbsoluteCropArea(i.cropArea,r);this.currentCropVariant=Object.assign({},i,{cropArea:o}),this.update(this.currentCropVariant)})).delegateTo(this.currentModal,"button[name=reset]"),ImageManipulation.isEmptyObject(this.currentCropVariant.cropArea)&&(this.defaultOpts=Object.assign({autoCropArea:1},this.defaultOpts)),this.cropper=new Cropper(t,Object.assign({},this.defaultOpts,{ready:()=>{this.cropBuiltHandler(),this.update(this.currentCropVariant)},crop:this.cropMoveHandler.bind(this),data:this.currentCropVariant.cropArea}))}update(t){const e=Object.assign({},t),r=t.allowedAspectRatios[t.selectedRatio];this.currentModal.querySelector("[data-bs-option].active")?.classList.remove("active"),this.currentModal.querySelector(`[data-bs-option="${t.selectedRatio}"]`)?.classList.add("active"),this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=Object.assign({},e,t),this.cropBox?.querySelector(this.coverAreaSelector)?.remove(),this.cropBox?.querySelectorAll(this.focusAreaSelector)?.length>0&&this.focusAreaEl.remove(),t.focusArea&&(ImageManipulation.isEmptyObject(t.focusArea)&&(this.currentCropVariant.focusArea=Object.assign({},this.defaultFocusArea)),this.focusAreaEl?.remove(),this.initFocusArea(this.cropBox)),t.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger)}initFocusArea(t){this.focusAreaEl=document.createElement("typo3-backend-draggable-resizable"),this.focusAreaEl.window=this.currentModal.ownerDocument.defaultView,this.focusAreaEl.offset=this.convertAreaToOffset(this.currentCropVariant.focusArea,t),this.focusAreaEl.container=t,this.focusAreaEl.pointerEventNames=ImageManipulation.resolvePointerEventNames(),this.focusAreaEl.addEventListener("draggable-resizable-started",(()=>{this.cropper.disable()})),this.focusAreaEl.addEventListener("draggable-resizable-updated",(()=>{const e=this.currentCropVariant.coverAreas,r=this.convertOffsetToArea(this.focusAreaEl.offset,t),a=this.focusAreaEl.querySelector(this.focusAreaSelector);this.checkFocusAndCoverAreasCollision(r,e)?a.classList.add("has-nodrop"):a.classList.remove("has-nodrop")})),this.focusAreaEl.addEventListener("draggable-resizable-finished",(e=>{const r=this.currentCropVariant.coverAreas,a=this.convertOffsetToArea(this.focusAreaEl.offset,t);this.checkFocusAndCoverAreasCollision(a,r)?this.focusAreaEl.revert(e.detail.originOffset):this.scaleAndMoveFocusArea(a);this.focusAreaEl.querySelector(this.focusAreaSelector).classList.remove("has-nodrop"),this.cropper.enable()})),t.appendChild(this.focusAreaEl),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)}initCoverAreas(t,e){e.forEach((e=>{const r={height:ImageManipulation.toCssPercent(e.height),left:ImageManipulation.toCssPercent(e.x),top:ImageManipulation.toCssPercent(e.y),width:ImageManipulation.toCssPercent(e.width)},a=html` +import{html}from"lit";import{unsafeHTML}from"lit/directives/unsafe-html.js";import{styleMap}from"lit/directives/style-map.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import RegularEvent from"@typo3/core/event/regular-event.js";import FormEngineValidation from"@typo3/backend/form-engine-validation.js";import Cropper from"cropperjs";import{default as Modal}from"@typo3/backend/modal.js";import"@typo3/backend/element/spinner-element.js";import{renderNodes}from"@typo3/core/lit-helper.js";import{Offset}from"@typo3/backend/element/draggable-resizable-element.js";class ImageManipulation{constructor(){this.initialized=!1,this.triggerListener=null,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.querySelector(".cropper-canvas img")?.classList.remove("cropper-hide"),this.imageOriginalSizeFactor=parseInt(e.dataset.originalWidth,10)/t.naturalWidth,this.cropVariantTriggers.forEach((e=>{const r=e.dataset.cropVariantId,a=this.convertRelativeToAbsoluteCropArea(this.data[r].cropArea,t),i=Object.assign({},this.data[r],{cropArea:a});this.updatePreviewThumbnail(i,e)})),this.currentCropVariant.cropArea=this.convertRelativeToAbsoluteCropArea(this.currentCropVariant.cropArea,t),this.cropBox=this.currentModal.querySelector(".cropper-crop-box"),this.setCropArea(this.currentCropVariant.cropArea),this.currentCropVariant.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.currentCropVariant.focusArea&&(ImageManipulation.isEmptyObject(this.currentCropVariant.focusArea)&&(this.currentCropVariant.focusArea=Object.assign({},this.defaultFocusArea)),this.focusAreaEl?.remove(),this.initFocusArea(this.cropBox)),this.currentCropVariant.selectedRatio&&this.currentModal.querySelector(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`)?.classList.add("active")},this.cropMoveHandler=t=>{if(!this.initialized)return;let e=Math.floor(t.detail.width),r=Math.floor(t.detail.height);(e<15||r<15)&&(e=Math.max(15,r),r=Math.max(15,e),this.cropper.setData({width:e,height:r})),this.currentCropVariant.cropArea=Object.assign({},this.currentCropVariant.cropArea,{width:Math.floor(e),height:Math.floor(r),x:Math.floor(t.detail.x),y:Math.floor(t.detail.y)}),this.focusAreaEl&&this.currentCropVariant?.focusArea&&(this.focusAreaEl.offset=this.convertAreaToOffset(this.currentCropVariant.focusArea,this.cropBox)),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant);const a=Math.round(this.currentCropVariant.cropArea.width*this.imageOriginalSizeFactor),i=Math.round(this.currentCropVariant.cropArea.height*this.imageOriginalSizeFactor);this.cropInfo.innerText=`${a}×${i} px`}}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))}static isEmptyObject(t){return!t||"object"!=typeof t||0===Object.keys(t).length||"{}"===JSON.stringify(t)}static resolvePointerEventNames(){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=e?["touchmove"]:["mousemove"],i=e?["touchstart"]:["mousedown"],o=e?["touchend","touchcancel"]:["mouseup"];return{touchStart:i,touchMove:a,touchEnd:o,pointerDown:r?["pointerdown"]:i,pointerMove:r?["pointermove"]:a,pointerUp:r?["pointerup","pointercancel"]:o}}initializeTrigger(){this.triggerListener||(this.triggerListener=new RegularEvent("click",((t,e)=>{t.preventDefault(),this.trigger=e,this.show()})),this.triggerListener.delegateTo(document,".t3js-image-manipulation-trigger"))}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.dataset,e=t.modalTitle,r=t.buttonPreviewText,a=t.buttonDismissText,i=t.buttonSaveText,o=t.url,s=JSON.parse(t.payload);this.currentModal=Modal.advanced({additionalCssClasses:["modal-image-manipulation","cropper"],buttons:[{btnClass:"btn-default float-start",name:"preview",icon:"actions-view",text:r},{btnClass:"btn-default",name:"dismiss",icon:"actions-close",text:a},{btnClass:"btn-primary",name:"save",icon:"actions-document-save",text:i}],content:html`<div class="modal-loading"><typo3-backend-spinner size="large"></typo3-backend-spinner></div>`,size:Modal.sizes.full,style:Modal.styles.dark,title:e,staticBackdrop:!0}),this.currentModal.addEventListener("typo3-modal-shown",(()=>{new AjaxRequest(o).post(s).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.dataset.cropVariants;if(!e)throw new TypeError("ImageManipulation: No cropVariants data found for image");this.data=ImageManipulation.isEmptyObject(this.data)?JSON.parse(e):this.data,this.cropVariantTriggers=this.currentModal.querySelectorAll(".t3js-crop-variant-trigger"),this.activeCropVariantTrigger=this.currentModal.querySelector(".t3js-crop-variant-trigger.is-active"),this.cropInfo=this.currentModal.querySelector(this.cropInfoSelector),this.currentCropVariant=this.data[this.activeCropVariantTrigger.dataset.cropVariantId],this.cropVariantTriggers.forEach((t=>t.addEventListener("click",(t=>{if(t.currentTarget.classList.contains("is-active"))return t.stopPropagation(),void t.preventDefault();this.activeCropVariantTrigger.classList.remove("is-active"),t.currentTarget.classList.add("is-active"),this.activeCropVariantTrigger=t.currentTarget;const e=this.data[this.activeCropVariantTrigger.dataset.cropVariantId],r=this.cropper.getImageData();e.cropArea=this.convertRelativeToAbsoluteCropArea(e.cropArea,r),this.currentCropVariant=Object.assign({},e),this.update(e)})))),new RegularEvent("click",((t,e)=>{const r=e.dataset.bsOption;this.handleAspectRatioChange(r)})).delegateTo(this.currentModal,"label[data-method=setAspectRatio]"),new RegularEvent("keydown",((t,e)=>{if(!["Enter","Space"].includes(t.code))return;t.preventDefault(),t.stopImmediatePropagation();const r=e.closest('label[data-method="setAspectRatio"]'),a=r.dataset.bsOption;r.querySelector("input").checked=!0,this.handleAspectRatioChange(a)})).delegateTo(this.currentModal,'label[data-method="setAspectRatio"] input[type="radio"]'),new RegularEvent("click",(()=>this.save(this.data))).delegateTo(this.currentModal,"button[name=save]"),this.trigger.dataset.previewUrl?new RegularEvent("click",(()=>this.openPreview(this.data))).delegateTo(this.currentModal,"button[name=preview]"):this.currentModal.querySelectorAll("button[name=preview]").forEach((t=>t.style.display="none")),new RegularEvent("click",(()=>this.currentModal.hideModal())).delegateTo(this.currentModal,"button[name=dismiss]"),new RegularEvent("click",((t,e)=>{const r=this.cropper.getImageData(),a=e.dataset.cropVariant;if(t.preventDefault(),t.stopPropagation(),!a)throw new TypeError("TYPO3 Cropper: No cropVariant data attribute found on reset element.");const i=JSON.parse(a),o=this.convertRelativeToAbsoluteCropArea(i.cropArea,r);this.currentCropVariant=Object.assign({},i,{cropArea:o}),this.update(this.currentCropVariant)})).delegateTo(this.currentModal,"button[name=reset]"),ImageManipulation.isEmptyObject(this.currentCropVariant.cropArea)&&(this.defaultOpts=Object.assign({autoCropArea:1},this.defaultOpts)),this.cropper=new Cropper(t,Object.assign({},this.defaultOpts,{ready:()=>{this.cropBuiltHandler(),this.update(this.currentCropVariant)},crop:this.cropMoveHandler.bind(this),data:this.currentCropVariant.cropArea}))}handleAspectRatioChange(t){const e=Object.assign({},this.currentCropVariant),r=e.allowedAspectRatios[t];this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=Object.assign({},e,{selectedRatio:t}),this.update(this.currentCropVariant)}update(t){const e=Object.assign({},t),r=t.allowedAspectRatios[t.selectedRatio];this.currentModal.querySelector("[data-bs-option].active")?.classList.remove("active"),this.currentModal.querySelector(`[data-bs-option="${t.selectedRatio}"]`)?.classList.add("active"),this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=Object.assign({},e,t),this.cropBox?.querySelector(this.coverAreaSelector)?.remove(),this.cropBox?.querySelectorAll(this.focusAreaSelector)?.length>0&&this.focusAreaEl.remove(),t.focusArea&&(ImageManipulation.isEmptyObject(t.focusArea)&&(this.currentCropVariant.focusArea=Object.assign({},this.defaultFocusArea)),this.focusAreaEl?.remove(),this.initFocusArea(this.cropBox)),t.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger)}initFocusArea(t){this.focusAreaEl=document.createElement("typo3-backend-draggable-resizable"),this.focusAreaEl.window=this.currentModal.ownerDocument.defaultView,this.focusAreaEl.offset=this.convertAreaToOffset(this.currentCropVariant.focusArea,t),this.focusAreaEl.container=t,this.focusAreaEl.pointerEventNames=ImageManipulation.resolvePointerEventNames(),this.focusAreaEl.addEventListener("draggable-resizable-started",(()=>{this.cropper.disable()})),this.focusAreaEl.addEventListener("draggable-resizable-updated",(()=>{const e=this.currentCropVariant.coverAreas,r=this.convertOffsetToArea(this.focusAreaEl.offset,t),a=this.focusAreaEl.querySelector(this.focusAreaSelector);this.checkFocusAndCoverAreasCollision(r,e)?a.classList.add("has-nodrop"):a.classList.remove("has-nodrop")})),this.focusAreaEl.addEventListener("draggable-resizable-finished",(e=>{const r=this.currentCropVariant.coverAreas,a=this.convertOffsetToArea(this.focusAreaEl.offset,t);this.checkFocusAndCoverAreasCollision(a,r)?this.focusAreaEl.revert(e.detail.originOffset):this.scaleAndMoveFocusArea(a);this.focusAreaEl.querySelector(this.focusAreaSelector).classList.remove("has-nodrop"),this.cropper.enable()})),t.appendChild(this.focusAreaEl),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea)}initCoverAreas(t,e){e.forEach((e=>{const r={height:ImageManipulation.toCssPercent(e.height),left:ImageManipulation.toCssPercent(e.x),top:ImageManipulation.toCssPercent(e.y),width:ImageManipulation.toCssPercent(e.width)},a=html` <div class="cropper-cover-area t3js-cropper-cover-area" style=${styleMap(r)}></div> `;this.renderElements(a,t)}))}updatePreviewThumbnail(t,e){const r=e.querySelector(".t3js-cropper-preview-thumbnail-crop-area"),a=e.querySelector(".t3js-cropper-preview-thumbnail-crop-image"),i=e.querySelector(".t3js-cropper-preview-thumbnail-focus-area"),o=this.cropper.getImageData();Object.assign(r.style,{height:ImageManipulation.toCssPercent(t.cropArea.height/o.naturalHeight),left:ImageManipulation.toCssPercent(t.cropArea.x/o.naturalWidth),top:ImageManipulation.toCssPercent(t.cropArea.y/o.naturalHeight),width:ImageManipulation.toCssPercent(t.cropArea.width/o.naturalWidth)}),t.focusArea&&Object.assign(i.style,{height:ImageManipulation.toCssPercent(t.focusArea.height),left:ImageManipulation.toCssPercent(t.focusArea.x),top:ImageManipulation.toCssPercent(t.focusArea.y),width:ImageManipulation.toCssPercent(t.focusArea.width)});const s=getComputedStyle(r),n={width:s.getPropertyValue("width"),height:s.getPropertyValue("height"),left:s.getPropertyValue("left"),top:s.getPropertyValue("top")};Object.assign(a.style,{height:parseFloat(n.height)*(1/(t.cropArea.height/o.naturalHeight))+"px",margin:-1*parseFloat(n.left)+"px",marginTop:-1*parseFloat(n.top)+"px",width:parseFloat(n.width)*(1/(t.cropArea.width/o.naturalWidth))+"px"})}scaleAndMoveFocusArea(t){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]=Object.assign({},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&&e.x<t.x+t.width&&t.y<e.y+e.height&&e.y<t.height+t.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").querySelector(`.t3js-image-manipulation-preview[data-crop-variant-id="${a}"]`),n=this.trigger.closest(".form-group").querySelector(`.t3js-image-manipulation-selected-ratio[data-crop-variant-id="${a}"]`);if(!(s instanceof HTMLElement))return;let c=s.getBoundingClientRect().width,h=parseInt(s.dataset.previewHeight,10);const l=o.width/o.height,p=c/l;p>h?c=h*l:h=p,c>o.width&&(c=o.width,h=o.height);const u=c/o.width,d={height:r.naturalHeight*u+"px",left:-o.x*u+"px",top:-o.y*u+"px",width:r.naturalWidth*u+"px"},g=html` <span class="thumbnail thumbnail-status"> -- GitLab