Files

14 lines
17 KiB
JavaScript

/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
import{html as f}from"lit";import{unsafeHTML as S}from"lit/directives/unsafe-html.js";import{styleMap as m}from"lit/directives/style-map.js";import O from"@typo3/core/ajax/ajax-request.js";import p from"@typo3/core/event/regular-event.js";import V from"@typo3/backend/form-engine.js";import w from"cropperjs";import v from"@typo3/backend/modal.js";import"@typo3/backend/element/spinner-element.js";import{renderNodes as x}from"@typo3/core/lit-helper.js";import{topLevelModuleImport as M}from"@typo3/backend/utility/top-level-module-import.js";import{Offset as P}from"@typo3/backend/offset.js";import b from"@typo3/backend/storage/browser-session.js";class c{constructor(){this.syncAvailable=!1,this.syncEnabled=!1,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="typo3-backend-draggable-resizable",this.focusAreaVisualElementSelector="typo3-backend-draggable-resizable .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(r=>{const a=r.dataset.cropVariantId,s=this.convertRelativeToAbsoluteCropArea(this.data[a].cropArea,t),i=Object.assign({},this.data[a],{cropArea:s});this.updatePreviewThumbnail(i,r),this.currentModal.querySelector(`[data-crop-variant-container="${i.id}"]`)?.querySelector(`[data-bs-option="${i.selectedRatio}"]`)?.classList.add("active")}),this.currentCropVariant.cropArea=this.convertRelativeToAbsoluteCropArea(this.currentCropVariant.cropArea,t),this.cropBox=this.currentModal.querySelector(".cropper-crop-box"),this.currentCropVariant.selectedRatio&&this.currentModal.querySelector(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`)?.classList.add("active")},this.cropMoveHandler=t=>{if(!this.initialized)return;const e=15,r=15;let a=Math.floor(t.detail.width),s=Math.floor(t.detail.height);if((a<e||s<r)&&(a=Math.max(r,s),s=Math.max(e,a),this.cropper.setData({width:a,height:s})),this.currentCropVariant.cropArea=Object.assign({},this.currentCropVariant.cropArea,{width:Math.floor(a),height:Math.floor(s),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.syncAvailable&&this.syncEnabled){const n=this.data;Object.keys(this.data).forEach(l=>{let h=null;this.cropVariantTriggers.forEach(d=>{d.dataset.cropVariantId===l&&(h=d)});const u=h.dataset.excludeFromSync.toLowerCase()==="true";if(h&&!u){const d=Object.assign({},n[l],{cropArea:this.currentCropVariant.cropArea,cropVariant:l});this.updatePreviewThumbnail(d,h),this.updateCropVariantData(d)}})}this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger),this.updateCropVariantData(this.currentCropVariant);const i=Math.round(this.currentCropVariant.cropArea.width*this.imageOriginalSizeFactor),o=Math.round(this.currentCropVariant.cropArea.height*this.imageOriginalSizeFactor);this.cropInfo.innerText=`${i}\xD7${o} px`}}static wait(t,e){window.setTimeout(t,e)}static toCssPercent(t){return`${t*100}%`}static serializeCropVariants(t){return JSON.stringify(t,(r,a)=>r==="id"||r==="title"||r==="allowedAspectRatios"||r==="coverAreas"?void 0:a)}static isEmptyObject(t){return!t||typeof t!="object"||Object.keys(t).length===0||JSON.stringify(t)==="{}"}static resolvePointerEventNames(){const t=typeof window<"u"&&typeof window.document<"u",e=t&&window.document.documentElement?"ontouchstart"in window.document.documentElement:!1,r=t?"PointerEvent"in window:!1,a=e?["touchmove"]:["mousemove"],s=e?["touchstart"]:["mousedown"],i=e?["touchend","touchcancel"]:["mouseup"];return{touchStart:s,touchMove:a,touchEnd:i,pointerDown:r?["pointerdown"]:s,pointerMove:r?["pointermove"]:a,pointerUp:r?["pointerup","pointercancel"]:i}}initializeTrigger(){this.triggerListener||(this.triggerListener=new p("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,s=t.buttonSaveText,i=t.url,o=JSON.parse(t.payload);this.currentModal=v.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:s}],content:f`<div class=modal-loading><typo3-backend-spinner size=large></typo3-backend-spinner></div>`,size:v.sizes.full,style:v.styles.dark,title:e,staticBackdrop:!0}),this.currentModal.addEventListener("typo3-modal-shown",()=>{new O(i).post(o).then(async n=>{const l=await n.resolve();this.currentModal.templateResultContent=f`${S(l)}`,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=c.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(a=>a.addEventListener("click",s=>{if(s.currentTarget.classList.contains("is-active")){s.stopPropagation(),s.preventDefault();return}this.activeCropVariantTrigger.classList.remove("is-active"),s.currentTarget.classList.add("is-active"),this.activeCropVariantTrigger=s.currentTarget;const i=this.data[this.activeCropVariantTrigger.dataset.cropVariantId],o=this.cropper.getImageData();i.cropArea=this.convertRelativeToAbsoluteCropArea(i.cropArea,o),this.currentCropVariant=Object.assign({},i),this.update(i)})),new p("click",(a,s)=>{const i=s.dataset.bsOption;this.handleAspectRatioChange(i)}).delegateTo(this.currentModal,"label[data-method=setAspectRatio]"),new p("keydown",(a,s)=>{if(!["Enter","Space"].includes(a.code))return;a.preventDefault(),a.stopImmediatePropagation();const i=s.closest('label[data-method="setAspectRatio"]'),o=i.dataset.bsOption;i.querySelector("input").checked=!0,this.handleAspectRatioChange(o)}).delegateTo(this.currentModal,'label[data-method="setAspectRatio"] input[type="radio"]'),new p("click",()=>this.save(this.data)).delegateTo(this.currentModal,"button[name=save]"),this.trigger.dataset.previewUrl?new p("click",()=>this.openPreview(this.data)).delegateTo(this.currentModal,"button[name=preview]"):this.currentModal.querySelectorAll("button[name=preview]").forEach(a=>a.style.display="none"),new p("click",()=>this.currentModal.hideModal()).delegateTo(this.currentModal,"button[name=dismiss]"),new p("click",(a,s)=>{const i=this.cropper.getImageData(),o=s.dataset.cropVariant;if(a.preventDefault(),a.stopPropagation(),!o)throw new TypeError("TYPO3 Cropper: No cropVariant data attribute found on reset element.");const n=JSON.parse(o),l=this.convertRelativeToAbsoluteCropArea(n.cropArea,i);this.currentCropVariant=Object.assign({},n,{cropArea:l}),this.update(this.currentCropVariant)}).delegateTo(this.currentModal,"button[name=reset]"),c.isEmptyObject(this.currentCropVariant.cropArea)&&(this.defaultOpts=Object.assign({autoCropArea:1},this.defaultOpts)),this.cropper=new w(t,Object.assign({},this.defaultOpts,{ready:()=>{this.cropBuiltHandler(),this.update(this.currentCropVariant)},crop:this.cropMoveHandler,data:this.currentCropVariant.cropArea})),this.syncAvailable=this.trigger.dataset.syncAvailable.toLowerCase()==="true";const r=this.trigger.dataset.imageUid;if(this.syncAvailable&&(this.currentModal.querySelector("#sync-crop-variants-container").classList.remove("d-none"),new p("click",(s,i)=>{if(s.stopPropagation(),this.syncEnabled=i.checked,b.set("sync-active-"+r,this.syncEnabled?"true":"false"),this.syncEnabled){if(this.currentModal.querySelectorAll('.panel-button[data-exclude-from-sync="true"]').forEach(n=>n.setAttribute("disabled","disabled")),this.currentModal.querySelector('.panel-button[data-crop-variant-id="'+this.currentCropVariant.id+'"][data-exclude-from-sync="true"]')!==null){const n=this.currentModal.querySelector('.panel-button[data-exclude-from-sync="false"]');n&&n.click()}}else this.currentModal.querySelectorAll(".panel-button[data-exclude-from-sync]").forEach(o=>o.removeAttribute("disabled"))}).delegateTo(this.currentModal,"#sync-crop-variants"),(b.get("sync-active-"+r)??"false")==="true")){const s=this.currentModal.querySelector("#sync-crop-variants");s&&window.setTimeout(()=>{s.click()},100)}}handleAspectRatioChange(t){const e=Object.assign({},this.currentCropVariant),r=e.allowedAspectRatios[t];if(this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=Object.assign({},e,{selectedRatio:t}),this.syncAvailable&&this.syncEnabled){const a=this.data;Object.keys(this.data).forEach(s=>{let i=null;this.cropVariantTriggers.forEach(n=>{n.dataset.cropVariantId===s&&(i=n)});const o=i.dataset.excludeFromSync.toLowerCase()==="true";if(i&&!o){const n=Object.assign({},a[s],{selectedRatio:t,cropArea:e.cropArea});this.update(n),this.updatePreviewThumbnail(n,i)}})}this.update(this.currentCropVariant)}async update(t){const e=Object.assign({},t),r=t.allowedAspectRatios[t.selectedRatio];this.cropInfo=this.currentModal.querySelector(`[data-crop-variant-container="${t.id}"]`)?.querySelector(this.cropInfoSelector),this.currentModal.querySelector(`[data-crop-variant-container="${t.id}"]`)?.querySelector("[data-bs-option].active")?.classList.remove("active"),this.currentModal.querySelector(`[data-crop-variant-container="${t.id}"]`)?.querySelector(`[data-bs-option="${t.selectedRatio}"]`)?.classList.add("active"),this.setAspectRatio(r),this.setCropArea(e.cropArea),this.currentCropVariant=Object.assign({},e,t),this.cropBox?.querySelectorAll(this.coverAreaSelector)?.forEach(a=>a.remove()),this.cropBox?.querySelectorAll(this.focusAreaSelector)?.forEach(a=>a.remove()),t.focusArea?(c.isEmptyObject(t.focusArea)&&(this.currentCropVariant.focusArea=Object.assign({},this.defaultFocusArea)),this.focusAreaEl=this.initFocusArea(this.cropBox)):this.focusAreaEl=null,t.coverAreas&&this.initCoverAreas(this.cropBox,this.currentCropVariant.coverAreas),this.updatePreviewThumbnail(this.currentCropVariant,this.activeCropVariantTrigger)}initFocusArea(t){M("@typo3/backend/element/draggable-resizable-element.js");const e=top.document.createElement("typo3-backend-draggable-resizable");return e.setAttribute("offset",JSON.stringify(this.convertAreaToOffset(this.currentCropVariant.focusArea,t))),e.setAttribute("pointereventnames",JSON.stringify(c.resolvePointerEventNames())),e.addEventListener("draggable-resizable-started",()=>{this.cropper.disable()}),e.addEventListener("draggable-resizable-updated",()=>{const r=this.currentCropVariant.coverAreas,a=this.convertOffsetToArea(e.offset,t),s=e.querySelector(this.focusAreaVisualElementSelector);this.checkFocusAndCoverAreasCollision(a,r)?s.classList.add("has-nodrop"):s.classList.remove("has-nodrop")}),e.addEventListener("draggable-resizable-finished",r=>{const a=this.currentCropVariant.coverAreas,s=this.convertOffsetToArea(e.offset,t);this.checkFocusAndCoverAreasCollision(s,a)?e.revert(r.detail.originOffset):this.scaleAndMoveFocusArea(s),e.querySelector(this.focusAreaVisualElementSelector).classList.remove("has-nodrop"),this.cropper.enable()}),t.appendChild(e),this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea),e}initCoverAreas(t,e){e.forEach(r=>{const a={height:c.toCssPercent(r.height),left:c.toCssPercent(r.x),top:c.toCssPercent(r.y),width:c.toCssPercent(r.width)},s=f`<div class="cropper-cover-area t3js-cropper-cover-area" style=${m(a)}></div>`;this.renderElements(s,t)})}updatePreviewThumbnail(t,e){const r=e.querySelector(".t3js-cropper-preview-thumbnail-crop-area"),a=e.querySelector(".t3js-cropper-preview-thumbnail-crop-image"),s=e.querySelector(".t3js-cropper-preview-thumbnail-focus-area"),i=this.cropper.getImageData();Object.assign(r.style,{height:c.toCssPercent(t.cropArea.height/i.naturalHeight),left:c.toCssPercent(t.cropArea.x/i.naturalWidth),top:c.toCssPercent(t.cropArea.y/i.naturalHeight),width:c.toCssPercent(t.cropArea.width/i.naturalWidth)}),t.focusArea&&Object.assign(s.style,{height:c.toCssPercent(t.focusArea.height),left:c.toCssPercent(t.focusArea.x),top:c.toCssPercent(t.focusArea.y),width:c.toCssPercent(t.focusArea.width)});const o=getComputedStyle(r),n={width:o.getPropertyValue("width"),height:o.getPropertyValue("height"),left:o.getPropertyValue("left"),top:o.getPropertyValue("top")};Object.assign(a.style,{height:`${parseFloat(n.height)*(1/(t.cropArea.height/i.naturalHeight))}px`,margin:`${-1*parseFloat(n.left)}px`,marginTop:`${-1*parseFloat(n.top)}px`,width:`${parseFloat(n.width)*(1/(t.cropArea.width/i.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];e.value===0?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(r=>t.x<r.x+r.width&&r.x<t.x+t.width&&t.y<r.y+r.height&&r.y<t.height+t.y):!1}convertAbsoluteToRelativeCropArea(t,e){const{height:r,width:a,x:s,y:i}=t;return{height:r/e.naturalHeight,width:a/e.naturalWidth,x:s/e.naturalWidth,y:i/e.naturalHeight}}convertRelativeToAbsoluteCropArea(t,e){const{height:r,width:a,x:s,y:i}=t;return{height:r*e.naturalHeight,width:a*e.naturalWidth,x:s*e.naturalWidth,y:i*e.naturalHeight}}setPreviewImages(t){const e=this.cropper.image,r=this.cropper.getImageData();Object.keys(t).forEach(a=>{const s=t[a],i=this.convertRelativeToAbsoluteCropArea(s.cropArea,r),o=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(!(o instanceof HTMLElement))return;let l=o.getBoundingClientRect().width,h=parseInt(o.dataset.previewHeight,10);const u=i.width/i.height,d=l/u;d>h?l=h*u:h=d,l>i.width&&(l=i.width,h=i.height);const g=l/i.width,C={height:`${r.naturalHeight*g}px`,left:`${-i.x*g}px`,top:`${-i.y*g}px`,width:`${r.naturalWidth*g}px`},A={width:`${l}px`,height:`${h}px`},T=f`<span class="thumbnail thumbnail-status"> <div class=cropper-preview-container style=${m(A)}><img src=${e.src} style=${m(C)}></div> </span>`;for(;o.firstChild;)o.removeChild(o.firstChild);this.renderElements(T,o);const E=this.currentModal.ownerDocument.defaultView,y=this.currentModal.querySelector(`.t3-js-ratio-title[data-ratio-id="${s.id}${s.selectedRatio}"]`);n instanceof HTMLElement&&y instanceof E.HTMLElement&&(n.innerText=y.innerText)})}openPreview(t){const e=c.serializeCropVariants(t);let r=this.trigger.dataset.previewUrl;r=r+(r.includes("?")?"&":"?")+"cropVariants="+encodeURIComponent(e),window.open(r,"TYPO3ImageManipulationPreview")}save(t){const e=c.serializeCropVariants(t),r=document.querySelector(`#${this.trigger.dataset.field}`);this.trigger.dataset.cropVariants=JSON.stringify(t),this.setPreviewImages(t),r.value=e,V.markFieldAsChanged(r),this.currentModal.hideModal()}destroy(){this.currentModal&&(this.cropper instanceof w&&this.cropper.destroy(),this.initialized=!1,this.cropper=null,this.currentModal=null,this.data=null)}convertAreaToOffset(t,e){const r=e.getBoundingClientRect();return new P(t.x*r.width,t.y*r.height,t.width*r.width,t.height*r.height)}convertOffsetToArea(t,e){const r=e.getBoundingClientRect();return{x:t.left/r.width,y:t.top/r.height,width:t.width/r.width,height:t.height/r.height}}renderElements(t,e,r){const a=x(t);return Array.from(a).filter(i=>i instanceof HTMLElement).forEach(i=>e.appendChild(i)),r?e.querySelector(r):null}}var R=new c;export{R as default};