web-mojo
Version:
WEB-MOJO - A lightweight JavaScript framework for building data-driven web applications
1 lines • 94.2 kB
JavaScript
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./chunks/WebApp-sKJf8j1s.js"),e=require("./chunks/Dialog-BmvpkgLD.js"),a=require("./chunks/PDFViewer-D5SmAaQD.js");class ImageViewer extends t.View{constructor(t={}){super({...t,className:`image-viewer ${t.className||""}`,tagName:"div"}),this.imageUrl=t.imageUrl||t.src||"",this.alt=t.alt||"Image",this.title=t.title||"",this.canvas=null,this.context=null,this.image=null,this.scale=1,this.rotation=0,this.translateX=0,this.translateY=0,this.minScale=.1,this.maxScale=5,this.scaleStep=.1,this.isDragging=!1,this.lastPointerX=0,this.lastPointerY=0,this.isLoaded=!1,this.showControls=!1!==t.showControls,this.allowRotate=!1!==t.allowRotate,this.allowZoom=!1!==t.allowZoom,this.allowPan=!1!==t.allowPan,this.allowDownload=!1!==t.allowDownload,this.autoFit=!1!==t.autoFit,this.containerElement=null,this.controlsElement=null}async getTemplate(){return'\n <div class="image-viewer-container d-flex flex-column h-100" data-container="imageContainer">\n <div class="image-viewer-content flex-grow-1 position-relative">\n <canvas class="image-viewer-canvas w-100 h-100" data-container="canvas"></canvas>\n <div class="image-viewer-overlay">\n <div class="image-viewer-loading">\n <div class="spinner-border text-light" role="status">\n <span class="visually-hidden">Loading...</span>\n </div>\n </div>\n </div>\n </div>\n\n {{#showControls}}\n <div class="image-viewer-controls position-absolute top-0 start-50 translate-middle-x mt-3" data-container="controls" style="z-index: 10;">\n <div class="btn-group" role="group">\n {{#allowZoom}}\n <button type="button" class="btn btn-dark btn-sm" data-action="zoom-in" title="Zoom In">\n <i class="bi bi-zoom-in"></i>\n </button>\n <button type="button" class="btn btn-dark btn-sm" data-action="zoom-out" title="Zoom Out">\n <i class="bi bi-zoom-out"></i>\n </button>\n <button type="button" class="btn btn-dark btn-sm" data-action="zoom-fit" title="Fit to Screen">\n <i class="bi bi-arrows-fullscreen"></i>\n </button>\n <button type="button" class="btn btn-dark btn-sm" data-action="zoom-actual" title="Actual Size">\n <i class="bi bi-1-square"></i>\n </button>\n {{/allowZoom}}\n\n {{#allowRotate}}\n <button type="button" class="btn btn-dark btn-sm" data-action="rotate-left" title="Rotate Left">\n <i class="bi bi-arrow-counterclockwise"></i>\n </button>\n <button type="button" class="btn btn-dark btn-sm" data-action="rotate-right" title="Rotate Right">\n <i class="bi bi-arrow-clockwise"></i>\n </button>\n {{/allowRotate}}\n\n <button type="button" class="btn btn-dark btn-sm" data-action="reset" title="Reset View">\n <i class="bi bi-arrow-repeat"></i>\n </button>\n\n {{#allowDownload}}\n <button type="button" class="btn btn-dark btn-sm" data-action="download" title="Download Image">\n <i class="bi bi-download"></i>\n </button>\n {{/allowDownload}}\n </div>\n\n <div class="image-viewer-info">\n <span class="zoom-level">{{scale}}%</span>\n </div>\n </div>\n {{/showControls}}\n </div>\n '}async onAfterRender(){this.canvas=this.element.querySelector(".image-viewer-canvas"),this.context=this.canvas.getContext("2d"),this.containerElement=this.element.querySelector(".image-viewer-content"),this.controlsElement=this.element.querySelector(".image-viewer-controls"),this.setupCanvas(),this.setupEventListeners(),this.imageUrl&&this.loadImage(this.imageUrl)}setupCanvas(){this.canvas&&this.containerElement&&(this.context&&(this.context.imageSmoothingEnabled=!0,this.context.imageSmoothingQuality="high"),setTimeout(()=>{this.resizeCanvas(),this.isLoaded&&this.image&&this.renderCanvas()},2e3))}resizeCanvas(){if(!this.canvas)return;const t=window.innerWidth,e=window.innerHeight,a=Math.floor(.8*t),i=Math.floor(.8*e);if(a===this.canvasWidth&&i===this.canvasHeight)return;const s=window.devicePixelRatio||1;this.canvasWidth=a,this.canvasHeight=i,this.canvas.width=a*s,this.canvas.height=i*s,this.canvas.style.width=a+"px",this.canvas.style.height=i+"px",this.context.setTransform(s,0,0,s,0,0),this.isLoaded&&this.image&&this.renderCanvas()}setupEventListeners(){this.canvas&&(this.allowPan&&(this.canvas.addEventListener("mousedown",t=>this.handleMouseDown(t)),document.addEventListener("mousemove",t=>this.handleMouseMove(t)),document.addEventListener("mouseup",t=>this.handleMouseUp(t))),this.allowZoom&&this.canvas.addEventListener("wheel",t=>this.handleWheel(t),{passive:!1}),this.canvas.addEventListener("touchstart",t=>this.handleTouchStart(t),{passive:!1}),this.canvas.addEventListener("touchmove",t=>this.handleTouchMove(t),{passive:!1}),this.canvas.addEventListener("touchend",t=>this.handleTouchEnd(t)),this.canvas.addEventListener("contextmenu",t=>t.preventDefault()))}async handleActionZoomIn(){this.zoomIn()}async handleActionZoomOut(){this.zoomOut()}async handleActionZoomFit(){this.fitToContainer()}async handleActionZoomActual(){this.setScale(1),this.renderCanvas()}async handleActionRotateLeft(){this.rotate(-90)}async handleActionRotateRight(){this.rotate(90)}async handleActionReset(){this.reset()}async handleActionDownload(){this.downloadImage()}loadImage(t){this.isLoaded=!1,this.element.classList.remove("loaded");const e=new Image;e.crossOrigin="anonymous",e.onload=()=>{this.image=e,this.handleImageLoad()},e.onerror=()=>{this.handleImageError()},e.src=t}handleImageLoad(){this.isLoaded=!0,this.element.classList.add("loaded");const t=()=>{this.canvasWidth&&this.canvasHeight||this.resizeCanvas(),this.autoFit?this.fitToContainer():this.smartFit(),this.renderCanvas(),this.updateControls()};this.canvasWidth&&this.canvasHeight?requestAnimationFrame(t):setTimeout(t,2e3);const e=this.getApp()?.events;e&&e.emit("imageviewer:loaded",{viewer:this,imageUrl:this.imageUrl,naturalWidth:this.image.naturalWidth,naturalHeight:this.image.naturalHeight})}handleImageError(){console.error("Failed to load image:",this.imageUrl);const t=this.getApp()?.events;t&&t.emit("imageviewer:error",{viewer:this,imageUrl:this.imageUrl,error:"Failed to load image"})}handleMouseDown(t){if(!this.allowPan||0!==t.button)return;t.preventDefault(),this.isDragging=!0;const e=this.canvas.getBoundingClientRect();this.lastPointerX=t.clientX-e.left,this.lastPointerY=t.clientY-e.top,this.canvas.style.cursor="grabbing"}handleMouseMove(t){if(!this.isDragging||!this.allowPan)return;t.preventDefault();const e=this.canvas.getBoundingClientRect(),a=t.clientX-e.left,i=t.clientY-e.top,s=a-this.lastPointerX,n=i-this.lastPointerY;this.pan(s,n),this.lastPointerX=a,this.lastPointerY=i}handleMouseUp(t){this.isDragging&&(this.isDragging=!1,this.canvas.style.cursor=this.allowPan?"grab":"default")}handleWheel(t){if(!this.allowZoom)return;t.preventDefault();const e=this.canvas.getBoundingClientRect(),a=t.clientX-e.left,i=t.clientY-e.top,s=t.deltaY>0?.5*-this.scaleStep:.5*this.scaleStep;this.zoomAtPoint(this.scale+s,a,i)}handleTouchStart(t){if(1===t.touches.length&&this.allowPan){t.preventDefault();const e=t.touches[0],a=this.canvas.getBoundingClientRect();this.isDragging=!0,this.lastPointerX=e.clientX-a.left,this.lastPointerY=e.clientY-a.top}}handleTouchMove(t){if(1===t.touches.length&&this.isDragging&&this.allowPan){t.preventDefault();const e=t.touches[0],a=this.canvas.getBoundingClientRect(),i=e.clientX-a.left,s=e.clientY-a.top,n=i-this.lastPointerX,o=s-this.lastPointerY;this.pan(n,o),this.lastPointerX=i,this.lastPointerY=s}}handleTouchEnd(t){this.isDragging=!1}zoomIn(){this.setScale(this.scale+this.scaleStep)}zoomOut(){this.setScale(this.scale-this.scaleStep)}setScale(t){const e=this.scale;this.scale=Math.max(this.minScale,Math.min(this.maxScale,t)),this.renderCanvas(),this.updateControls();const a=this.getApp()?.events;a&&e!==this.scale&&a.emit("imageviewer:scale-changed",{viewer:this,oldScale:e,newScale:this.scale})}zoomAtPoint(t,e,a){if(!this.image)return;const i=this.scale;if(this.setScale(t),i!==this.scale){const t=this.scale/i,s=this.canvasWidth/2,n=this.canvasHeight/2;this.translateX=(this.translateX-(e-s))*t+(e-s),this.translateY=(this.translateY-(a-n))*t+(a-n),this.renderCanvas()}}pan(t,e){this.translateX+=t,this.translateY+=e,this.renderCanvas()}rotate(t){const e=this.rotation;this.rotation=(this.rotation+t)%360,this.rotation<0&&(this.rotation+=360),this.renderCanvas();const a=this.getApp()?.events;a&&a.emit("imageviewer:rotated",{viewer:this,oldRotation:e,newRotation:this.rotation,degrees:t})}center(){this.translateX=0,this.translateY=0,this.renderCanvas()}fitToContainer(){if(!this.image||!this.canvasWidth||!this.canvasHeight)return;const t=this.canvasWidth-40,e=this.canvasHeight-40,a=t/this.image.naturalWidth,i=e/this.image.naturalHeight,s=Math.min(a,i,1);this.setScale(s),this.renderCanvas()}smartFit(){if(!this.image||!this.canvasWidth||!this.canvasHeight)return;const t=(this.canvasWidth-80)/this.image.naturalWidth,e=(this.canvasHeight-80)/this.image.naturalHeight,a=Math.min(t,e);a<1&&this.setScale(a),this.renderCanvas()}reset(){this.scale=1,this.rotation=0,this.translateX=0,this.translateY=0,this.renderCanvas(),this.updateControls()}renderCanvas(){this.context&&this.canvasWidth&&this.canvasHeight&&(this.context.clearRect(0,0,this.canvasWidth,this.canvasHeight),this.image&&this.isLoaded&&(this.context.save(),this.context.translate(this.canvasWidth/2+this.translateX,this.canvasHeight/2+this.translateY),this.context.scale(this.scale,this.scale),this.context.rotate(this.rotation*Math.PI/180),this.context.drawImage(this.image,-this.image.naturalWidth/2,-this.image.naturalHeight/2),this.context.restore()))}downloadImage(){if(this.canvas)try{const t=document.createElement("a");t.download=this.getDownloadFilename(),t.href=this.canvas.toDataURL("image/png"),document.body.appendChild(t),t.click(),document.body.removeChild(t);const e=this.getApp()?.events;e&&e.emit("imageviewer:downloaded",{viewer:this,filename:t.download})}catch(t){console.error("Failed to download image:",t);const e=this.getApp()?.events;e&&e.emit("imageviewer:download-error",{viewer:this,error:t.message})}}getDownloadFilename(){if(this.title)return`${this.title.replace(/[^a-z0-9]/gi,"_").toLowerCase()}.png`;try{const t=new URL(this.imageUrl).pathname.split("/").pop();if(t&&t.includes("."))return t.replace(/\.[^.]+$/,".png")}catch(t){}return"image.png"}updateControls(){if(!this.controlsElement)return;const t=this.controlsElement.querySelector(".zoom-level");t&&(t.textContent=`${Math.round(100*this.scale)}%`);const e=this.controlsElement.querySelector('[data-action="zoom-in"]'),a=this.controlsElement.querySelector('[data-action="zoom-out"]');e&&(e.disabled=this.scale>=this.maxScale),a&&(a.disabled=this.scale<=this.minScale)}setImage(t,e="",a=""){const i=this.imageUrl;this.imageUrl=t,this.alt=e,this.title=a,this.reset(),this.loadImage(t);const s=this.getApp()?.events;s&&s.emit("imageviewer:image-changed",{viewer:this,oldImageUrl:i,newImageUrl:t})}getCurrentState(){return{scale:this.scale,rotation:this.rotation,translateX:this.translateX,translateY:this.translateY}}setState(t){void 0!==t.scale&&(this.scale=t.scale),void 0!==t.rotation&&(this.rotation=t.rotation),void 0!==t.translateX&&(this.translateX=t.translateX),void 0!==t.translateY&&(this.translateY=t.translateY),this.renderCanvas(),this.updateControls()}async onBeforeDestroy(){this.isDragging&&(this.isDragging=!1);const t=this.getApp()?.events;t&&t.emit("imageviewer:destroyed",{viewer:this})}static async showDialog(t,a={}){const{title:i="Image Viewer",alt:s="Image",size:n="fullscreen",showControls:o=!0,allowRotate:r=!0,allowZoom:l=!0,allowPan:h=!0,allowDownload:c=!0,...d}=a,m=new ImageViewer({imageUrl:t,alt:s,title:i,showControls:o,allowRotate:r,allowZoom:l,allowPan:h,allowDownload:c,autoFit:!0});return e.default.showDialog({title:i,body:m,size:n,centered:!0,backdrop:"static",keyboard:!0,buttons:[{text:"Close",action:"close",class:"btn btn-secondary",dismiss:!0}],...d})}}window.ImageViewer=ImageViewer;class ImageCanvasView extends t.View{constructor(t={}){super({...t,className:`image-canvas-view ${t.className||""}`,tagName:"div"}),this.imageUrl=t.imageUrl||t.src||"",this.alt=t.alt||"Image",this.title=t.title||"",this.canvas=null,this.context=null,this.image=null,this.canvasWidth=0,this.canvasHeight=0,this.maxCanvasHeightPercent=t.maxCanvasHeightPercent||.7,this.maxCanvasWidthPercent=t.maxCanvasWidthPercent||.8,this.canvasSizes={sm:{width:400,height:300},md:{width:600,height:450},lg:{width:800,height:600},xl:{width:1e3,height:750},fullscreen:{width:0,height:0},auto:{width:0,height:0}},this.canvasSize=t.canvasSize||"auto",this.isLoaded=!1,this.isRendering=!1,this.autoFit=!1!==t.autoFit,this.crossOrigin=t.crossOrigin||"anonymous"}async getTemplate(){return'\n <div class="image-canvas-container d-flex flex-column h-100">\n <div class="image-canvas-content flex-grow-1 position-relative d-flex justify-content-center align-items-center">\n <canvas class="image-canvas w-100 h-100" data-container="canvas"></canvas>\n\n \x3c!-- Loading Overlay --\x3e\n <div class="image-canvas-loading position-absolute top-50 start-50 translate-middle"\n style="display: none; z-index: 10;">\n <div class="spinner-border text-primary" role="status">\n <span class="visually-hidden">Loading...</span>\n </div>\n </div>\n </div>\n </div>\n '}async onAfterRender(){this.canvas=this.element.querySelector("canvas"),this.context=this.canvas.getContext("2d"),this.containerElement=this.element.querySelector(".image-canvas-content"),this.loadingElement=this.element.querySelector(".image-canvas-loading"),this.setupCanvas(),this.imageUrl&&this.loadImage(this.imageUrl)}setupCanvas(){this.canvas&&this.containerElement&&(this.setCanvasSize(this.canvasSize),"fullscreen"===this.canvasSize&&(this._resizeHandler=()=>this.setCanvasSize("fullscreen"),window.addEventListener("resize",this._resizeHandler)),this.context.imageSmoothingEnabled=!0,this.context.imageSmoothingQuality="high")}setCanvasSize(t){const e=this.canvasSizes[t];if(!e&&"auto"!==t)return;let a,i;if("fullscreen"===t)a=Math.min(1200,.9*window.innerWidth),i=Math.min(900,.8*window.innerHeight);else if("auto"!==t&&e){const t=window.innerWidth*this.maxCanvasWidthPercent,s=window.innerHeight*this.maxCanvasHeightPercent;if(e.width>t||e.height>s)if(this.image){const e=t/this.image.naturalWidth,n=s/this.image.naturalHeight,o=Math.min(e,n,1);a=Math.floor(this.image.naturalWidth*o),i=Math.floor(this.image.naturalHeight*o),a=Math.max(300,a),i=Math.max(200,i)}else a=Math.min(600,t),i=Math.min(450,s);else a=e.width,i=e.height}else if(this.image){const t=window.innerWidth*this.maxCanvasWidthPercent,e=window.innerHeight*this.maxCanvasHeightPercent,s=t/this.image.naturalWidth,n=e/this.image.naturalHeight,o=Math.min(s,n,1);a=Math.floor(this.image.naturalWidth*o),i=Math.floor(this.image.naturalHeight*o),a=Math.max(300,a),i=Math.max(200,i)}else a=Math.min(600,window.innerWidth*this.maxCanvasWidthPercent),i=Math.min(450,window.innerHeight*this.maxCanvasHeightPercent);if(a=Math.min(a,window.innerWidth*this.maxCanvasWidthPercent),i=Math.min(i,window.innerHeight*this.maxCanvasHeightPercent),Math.abs(a-this.canvasWidth)<10&&Math.abs(i-this.canvasHeight)<10)return;const s=window.devicePixelRatio||1;this.canvasWidth=a,this.canvasHeight=i,this.canvas.width=a*s,this.canvas.height=i*s,this.canvas.style.width=a+"px",this.canvas.style.height=i+"px",this.context.setTransform(s,0,0,s,0,0),this.isLoaded&&this.renderCanvas()}loadImage(t){if(!t)return;this.imageUrl=t,this.isLoaded=!1,this.element.classList.remove("loaded"),this.showLoading();const e=new Image;this.crossOrigin&&(e.crossOrigin=this.crossOrigin),e.onload=()=>{this.image=e,this.handleImageLoad()},e.onerror=()=>{this.handleImageError()},e.src=t}handleImageLoad(){this.isLoaded=!0,this.element.classList.add("loaded"),this.hideLoading(),"auto"===this.canvasSize?this.setCanvasSize("auto"):this.autoFit&&this.fitToContainer(),this.renderCanvas();const t=this.getApp()?.events;t&&t.emit("imagecanvas:loaded",{view:this,imageUrl:this.imageUrl,naturalWidth:this.image.naturalWidth,naturalHeight:this.image.naturalHeight})}handleImageError(){console.error("Failed to load image:",this.imageUrl),this.hideLoading();const t=this.getApp()?.events;t&&t.emit("imagecanvas:error",{view:this,imageUrl:this.imageUrl,error:"Failed to load image"})}showLoading(){this.loadingElement&&(this.loadingElement.style.display="block")}hideLoading(){this.loadingElement&&(this.loadingElement.style.display="none")}renderCanvas(){this.context&&this.canvasWidth&&this.canvasHeight&&!this.isRendering&&(this.isRendering=!0,this.context.clearRect(0,0,this.canvasWidth,this.canvasHeight),this.image&&this.isLoaded?(this.context.save(),this.renderImage(),this.context.restore(),this.isRendering=!1):this.isRendering=!1)}renderImage(){if(!this.image)return;const t=this.canvasWidth/this.image.naturalWidth,e=this.canvasHeight/this.image.naturalHeight,a=Math.min(t,e,1),i=this.image.naturalWidth*a,s=this.image.naturalHeight*a,n=(this.canvasWidth-i)/2,o=(this.canvasHeight-s)/2;this.context.drawImage(this.image,n,o,i,s)}fitToContainer(){this.image&&this.canvasWidth&&this.canvasHeight&&(this.canvasWidth,this.canvasHeight,this.image.naturalWidth,this.image.naturalHeight,"auto"===this.canvasSize&&this.setCanvasSize("auto"),this.renderCanvas())}center(){this.renderCanvas()}reset(){this.renderCanvas()}exportImageData(){if(!this.canvas)return null;try{return this.canvas.toDataURL("image/png")}catch(t){return console.error("Failed to export image data:",t),null}}exportImageBlob(t=.9){return this.canvas?new Promise(e=>{try{this.canvas.toBlob(t=>{e(t)},"image/png",t)}catch(a){console.error("Failed to export image blob:",a),e(null)}}):Promise.resolve(null)}setImage(t,e="",a=""){const i=this.imageUrl;this.alt=e,this.title=a,this.loadImage(t);const s=this.getApp()?.events;s&&s.emit("imagecanvas:image-changed",{view:this,oldImageUrl:i,newImageUrl:t})}getImageData(){return{imageUrl:this.imageUrl,alt:this.alt,title:this.title,naturalWidth:this.image?.naturalWidth||0,naturalHeight:this.image?.naturalHeight||0,isLoaded:this.isLoaded}}async onBeforeDestroy(){this.isLoaded=!1,this.isRendering=!1,this.image=null,this._resizeHandler&&window.removeEventListener("resize",this._resizeHandler);const t=this.getApp()?.events;t&&t.emit("imagecanvas:destroyed",{view:this})}}window.ImageCanvasView=ImageCanvasView;class ImageTransformView extends ImageCanvasView{constructor(t={}){super({...t,className:`image-transform-view ${t.className||""}`}),this.scale=1,this.rotation=0,this.translateX=0,this.translateY=0,this.minScale=.1,this.maxScale=5,this.scaleStep=.02,this.isDragging=!1,this.lastPointerX=0,this.lastPointerY=0,this.allowPan=!1!==t.allowPan,this.allowZoom=!1!==t.allowZoom,this.allowRotate=!1!==t.allowRotate,this.allowKeyboard=!1!==t.allowKeyboard,this._handleMouseMove=this.handleMouseMove.bind(this),this._handleMouseUp=this.handleMouseUp.bind(this),this._handleKeyboard=this.handleKeyboard.bind(this),t.maxCanvasHeightPercent||(this.maxCanvasHeightPercent=.6)}async getTemplate(){return'\n <div class="image-transform-container d-flex flex-column h-100">\n \x3c!-- Transform Toolbar --\x3e\n <div class="image-transform-toolbar bg-light border-bottom p-2">\n <div class="btn-toolbar justify-content-center" role="toolbar">\n <div class="btn-group me-2" role="group" aria-label="Zoom controls">\n <button type="button" class="btn btn-outline-primary btn-sm" data-action="zoom-in" title="Zoom In">\n <i class="bi bi-zoom-in"></i>\n </button>\n <button type="button" class="btn btn-outline-primary btn-sm" data-action="zoom-out" title="Zoom Out">\n <i class="bi bi-zoom-out"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="fit-to-screen" title="Fit to Screen">\n <i class="bi bi-arrows-fullscreen"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="actual-size" title="Actual Size">\n <i class="bi bi-1-square"></i>\n </button>\n </div>\n\n <div class="btn-group me-2" role="group" aria-label="Rotate controls">\n <button type="button" class="btn btn-outline-info btn-sm" data-action="rotate-left" title="Rotate Left">\n <i class="bi bi-arrow-counterclockwise"></i>\n </button>\n <button type="button" class="btn btn-outline-info btn-sm" data-action="rotate-right" title="Rotate Right">\n <i class="bi bi-arrow-clockwise"></i>\n </button>\n </div>\n\n <div class="btn-group" role="group" aria-label="Position controls">\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="center-image" title="Center Image">\n <i class="bi bi-bullseye"></i>\n </button>\n </div>\n </div>\n </div>\n\n \x3c!-- Canvas Area --\x3e\n <div class="image-canvas-content flex-grow-1 position-relative d-flex justify-content-center align-items-center">\n <canvas class="image-canvas" data-container="canvas"></canvas>\n\n \x3c!-- Loading Overlay --\x3e\n <div class="image-canvas-loading position-absolute top-50 start-50 translate-middle"\n style="display: none; z-index: 10;">\n <div class="spinner-border text-primary" role="status">\n <span class="visually-hidden">Loading...</span>\n </div>\n </div>\n </div>\n </div>\n '}async onAfterRender(){await super.onAfterRender(),this.setupInteractionListeners()}setupInteractionListeners(){this.canvas&&(this.allowPan&&(this.canvas.addEventListener("mousedown",t=>this.handleMouseDown(t)),document.addEventListener("mousemove",this._handleMouseMove),document.addEventListener("mouseup",this._handleMouseUp)),this.allowZoom&&this.canvas.addEventListener("wheel",t=>this.handleWheel(t),{passive:!1}),this.canvas.addEventListener("touchstart",t=>this.handleTouchStart(t),{passive:!1}),this.canvas.addEventListener("touchmove",t=>this.handleTouchMove(t),{passive:!1}),this.canvas.addEventListener("touchend",t=>this.handleTouchEnd(t)),this.allowKeyboard&&document.addEventListener("keydown",this._handleKeyboard),this.canvas.addEventListener("contextmenu",t=>t.preventDefault()),this.canvas.style.cursor=this.allowPan?"grab":"default")}renderImage(){this.image&&(this.context.translate(this.canvasWidth/2+this.translateX,this.canvasHeight/2+this.translateY),this.context.scale(this.scale,this.scale),this.context.rotate(this.rotation*Math.PI/180),this.context.drawImage(this.image,-this.image.naturalWidth/2,-this.image.naturalHeight/2))}handleMouseDown(t){if(!this.allowPan||0!==t.button)return;t.preventDefault(),this.isDragging=!0;const e=this.canvas.getBoundingClientRect();this.lastPointerX=t.clientX-e.left,this.lastPointerY=t.clientY-e.top,this.canvas.style.cursor="grabbing"}handleMouseMove(t){if(!this.isDragging||!this.allowPan)return;t.preventDefault();const e=this.canvas.getBoundingClientRect(),a=t.clientX-e.left,i=t.clientY-e.top,s=a-this.lastPointerX,n=i-this.lastPointerY;this.pan(s,n),this.lastPointerX=a,this.lastPointerY=i}handleMouseUp(t){this.isDragging&&(this.isDragging=!1,this.canvas.style.cursor=this.allowPan?"grab":"default")}handleWheel(t){if(!this.allowZoom)return;t.preventDefault();const e=this.canvas.getBoundingClientRect(),a=t.clientX-e.left,i=t.clientY-e.top,s=t.deltaY>0?.5*-this.scaleStep:.5*this.scaleStep;this.zoomAtPoint(this.scale+s,a,i)}handleTouchStart(t){if(1===t.touches.length&&this.allowPan){t.preventDefault();const e=t.touches[0],a=this.canvas.getBoundingClientRect();this.isDragging=!0,this.lastPointerX=e.clientX-a.left,this.lastPointerY=e.clientY-a.top}}handleTouchMove(t){if(1===t.touches.length&&this.isDragging&&this.allowPan){t.preventDefault();const e=t.touches[0],a=this.canvas.getBoundingClientRect(),i=e.clientX-a.left,s=e.clientY-a.top,n=i-this.lastPointerX,o=s-this.lastPointerY;this.pan(n,o),this.lastPointerX=i,this.lastPointerY=s}}handleTouchEnd(t){this.isDragging=!1}handleKeyboard(t){if("INPUT"!==t.target.tagName&&"TEXTAREA"!==t.target.tagName)switch(t.key){case"+":case"=":this.allowZoom&&(t.preventDefault(),this.zoomIn());break;case"-":this.allowZoom&&(t.preventDefault(),this.zoomOut());break;case"0":t.preventDefault(),this.fitToContainer();break;case"1":t.preventDefault(),this.actualSize();break;case"r":case"R":this.allowRotate&&(t.preventDefault(),this.rotateRight())}}zoomIn(){this.setScale(this.scale+this.scaleStep)}zoomOut(){this.setScale(this.scale-this.scaleStep)}setScale(t){const e=this.scale;this.scale=Math.max(this.minScale,Math.min(this.maxScale,t)),e!==this.scale&&(this.renderCanvas(),this.emitTransformEvent("scale-changed",{oldScale:e,newScale:this.scale}))}zoomAtPoint(t,e,a){if(!this.image)return;const i=this.scale;if(this.setScale(t),i!==this.scale){const t=this.scale/i,s=this.canvasWidth/2,n=this.canvasHeight/2;this.translateX=(this.translateX-(e-s))*t+(e-s),this.translateY=(this.translateY-(a-n))*t+(a-n),this.renderCanvas()}}pan(t,e){this.translateX+=t,this.translateY+=e,this.renderCanvas(),this.emitTransformEvent("panned",{deltaX:t,deltaY:e})}rotate(t){const e=this.rotation;this.rotation=(this.rotation+t)%360,this.rotation<0&&(this.rotation+=360),this.renderCanvas(),this.emitTransformEvent("rotated",{oldRotation:e,newRotation:this.rotation,degrees:t})}rotateLeft(){this.rotate(-90)}rotateRight(){this.rotate(90)}center(){this.translateX=0,this.translateY=0,this.renderCanvas(),this.emitTransformEvent("centered")}actualSize(){this.setScale(1),this.center()}fitToContainer(){if(!this.image||!this.canvasWidth||!this.canvasHeight)return;const t=this.canvasWidth-40,e=this.canvasHeight-40,a=t/this.image.naturalWidth,i=e/this.image.naturalHeight,s=Math.min(a,i,1);this.setScale(s),this.center()}smartFit(){if(!this.image||!this.canvasWidth||!this.canvasHeight)return;const t=(this.canvasWidth-80)/this.image.naturalWidth,e=(this.canvasHeight-80)/this.image.naturalHeight,a=Math.min(t,e);a<1&&this.setScale(a),this.center()}reset(){this.scale=1,this.rotation=0,this.translateX=0,this.translateY=0,this.renderCanvas(),this.emitTransformEvent("reset")}handleImageLoad(){super.handleImageLoad(),this.autoFit?this.fitToContainer():this.smartFit()}getTransformState(){return{scale:this.scale,rotation:this.rotation,translateX:this.translateX,translateY:this.translateY}}setTransformState(t){void 0!==t.scale&&(this.scale=t.scale),void 0!==t.rotation&&(this.rotation=t.rotation),void 0!==t.translateX&&(this.translateX=t.translateX),void 0!==t.translateY&&(this.translateY=t.translateY),this.renderCanvas()}emitTransformEvent(t,e={}){const a=this.getApp()?.events;a&&a.emit(`imagetransform:${t}`,{view:this,transform:this.getTransformState(),...e})}async handleActionZoomIn(){this.zoomIn()}async handleActionZoomOut(){this.zoomOut()}async handleActionFitToScreen(){this.fitToContainer()}async handleActionActualSize(){this.actualSize()}async handleActionRotateLeft(){this.rotateLeft()}async handleActionRotateRight(){this.rotateRight()}async handleActionCenterImage(){this.center()}async onBeforeDestroy(){await super.onBeforeDestroy(),this.isDragging&&(this.isDragging=!1),document.removeEventListener("mousemove",this._handleMouseMove),document.removeEventListener("mouseup",this._handleMouseUp),document.removeEventListener("keydown",this._handleKeyboard),this.emitTransformEvent("destroyed")}static async showDialog(t,a={}){const{title:i="Transform Image",alt:s="Image",size:n="xl",allowPan:o=!0,allowZoom:r=!0,allowRotate:l=!0,...h}=a,c=new ImageTransformView({imageUrl:t,alt:s,title:i,allowPan:o,allowZoom:r,allowRotate:l}),d=new e.default({title:i,body:c,size:n,centered:!0,backdrop:"static",keyboard:!0,noBodyPadding:!0,maxCanvasHeightPercent:.5,buttons:[{text:"Cancel",action:"cancel",class:"btn btn-secondary",dismiss:!0},{text:"Apply Transform",action:"apply-transform",class:"btn btn-primary"}],...h});return await d.render(!0,document.body),d.show(),new Promise(t=>{d.on("hidden",()=>{d.destroy(),t({action:"cancel",view:c})}),d.on("action:cancel",()=>{d.hide()}),d.on("action:apply-transform",async()=>{const e=c.exportImageData();d.hide(),t({action:"transform",view:c,data:e,transformState:c.getTransformState()})})})}}window.ImageTransformView=Image;class ImageCropView extends ImageCanvasView{constructor(t={}){super({...t,className:`image-crop-view ${t.className||""}`}),this.originalImageUrl=t.imageUrl,this.cropMode=!1,this.cropBox={x:0,y:0,width:0,height:0},this.aspectRatio=t.aspectRatio||null,this.minCropSize=t.minCropSize||50,this.fixedCropSize=t.fixedCropSize||null,this.cropAndScale=t.cropAndScale||null,this.isDragging=!1,this.isResizing=!1,this.dragHandle=null,this.dragStartImageX=0,this.dragStartImageY=0,this.initialCropBox=null,this.newCropStart=null,this.handles={nw:{cursor:"nw-resize",x:0,y:0},ne:{cursor:"ne-resize",x:1,y:0},sw:{cursor:"sw-resize",x:0,y:1},se:{cursor:"se-resize",x:1,y:1},n:{cursor:"n-resize",x:.5,y:0},s:{cursor:"s-resize",x:.5,y:1},w:{cursor:"w-resize",x:0,y:.5},e:{cursor:"e-resize",x:1,y:.5}},this.handleSize=t.handleSize||12,this.showGrid=!1!==t.showGrid,this.showToolbar=!1!==t.showToolbar,this.autoFit=!1!==t.autoFit,this.imageOffsetX=0,this.imageOffsetY=0,this._handleMouseMove=this.handleMouseMove.bind(this),this._handleMouseUp=this.handleMouseUp.bind(this),!t.maxCanvasHeightPercent&&this.showToolbar&&(this.maxCanvasHeightPercent=.6)}imageToCanvas(t){if(!this.image)return t;const e=this.canvasWidth/this.image.naturalWidth,a=this.canvasHeight/this.image.naturalHeight;let i;i=this.autoFit?Math.min(e,a,1):1;const s=this.image.naturalWidth*i,n=this.image.naturalHeight*i,o=(this.canvasWidth-s)/2,r=(this.canvasHeight-n)/2;return{x:t.x*i+o,y:t.y*i+r,width:t.width*i,height:t.height*i}}canvasToImage(t){if(!this.image)return t;const e=this.canvasWidth/this.image.naturalWidth,a=this.canvasHeight/this.image.naturalHeight;let i;i=this.autoFit?Math.min(e,a,1):1;const s=this.image.naturalWidth*i,n=this.image.naturalHeight*i,o=(this.canvasWidth-s)/2,r=(this.canvasHeight-n)/2;return{x:(t.x-o)/i,y:(t.y-r)/i,width:t.width/i,height:t.height/i}}pointCanvasToImage(t,e){const a=this.canvasToImage({x:t,y:e,width:0,height:0});return{x:a.x,y:a.y}}async getTemplate(){return'\n <div class="image-crop-container d-flex flex-column h-100">\n {{#showToolbar}}\n \x3c!-- Crop Toolbar --\x3e\n <div class="image-crop-toolbar bg-light border-bottom p-2">\n <div class="btn-toolbar justify-content-center" role="toolbar">\n <div class="btn-group me-2" role="group" aria-label="Aspect ratio">\n <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"\n data-bs-toggle="dropdown" title="Aspect Ratio">\n <i class="bi bi-aspect-ratio"></i> Ratio\n </button>\n <ul class="dropdown-menu">\n <li><a class="dropdown-item" href="#" data-action="set-aspect-ratio" data-ratio="free">Free</a></li>\n <li><a class="dropdown-item" href="#" data-action="set-aspect-ratio" data-ratio="1">1:1 Square</a></li>\n <li><a class="dropdown-item" href="#" data-action="set-aspect-ratio" data-ratio="1.333">4:3</a></li>\n <li><a class="dropdown-item" href="#" data-action="set-aspect-ratio" data-ratio="1.777">16:9</a></li>\n <li><a class="dropdown-item" href="#" data-action="set-aspect-ratio" data-ratio="0.75">3:4 Portrait</a></li>\n </ul>\n </div>\n\n <div class="btn-group me-2" role="group" aria-label="Fit mode">\n <button type="button" class="btn btn-outline-info btn-sm" data-action="toggle-auto-fit" title="Toggle Auto-fit">\n <i class="bi bi-arrows-fullscreen"></i> <span class="auto-fit-text">Fit</span>\n </button>\n </div>\n\n <div class="btn-group me-2" role="group" aria-label="Crop actions">\n <button type="button" class="btn btn-success btn-sm" data-action="apply-crop" title="Apply Crop">\n <i class="bi bi-check"></i> Apply\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="reset-crop" title="Reset Crop">\n <i class="bi bi-arrow-repeat"></i> Reset\n </button>\n </div>\n </div>\n </div>\n {{/showToolbar}}\n\n \x3c!-- Canvas Area --\x3e\n <div class="image-canvas-content flex-grow-1 position-relative d-flex justify-content-center align-items-center">\n <canvas class="image-crop-canvas" data-container="canvas"></canvas>\n\n \x3c!-- Loading Overlay --\x3e\n <div class="image-canvas-loading position-absolute top-50 start-50 translate-middle"\n style="display: none; z-index: 10;">\n <div class="spinner-border text-primary" role="status">\n <span class="visually-hidden">Loading...</span>\n </div>\n </div>\n </div>\n </div>\n '}async onAfterRender(){await super.onAfterRender(),this.setupCropListeners(),this.updateAutoFitButtonState()}updateAutoFitButtonState(){if(!this.showToolbar)return;const t=this.element.querySelector('[data-action="toggle-auto-fit"]'),e=t?.querySelector(".auto-fit-text");t&&e&&(this.autoFit?(t.classList.remove("btn-outline-warning"),t.classList.add("btn-outline-info"),t.title="Toggle Auto-fit (currently: fit to canvas)",e.textContent="Fit"):(t.classList.remove("btn-outline-info"),t.classList.add("btn-outline-warning"),t.title="Toggle Auto-fit (currently: actual size)",e.textContent="1:1"))}handleImageLoad(){super.handleImageLoad(),this.updateImageOffset(),setTimeout(()=>{this.isLoaded&&this.canvasWidth>0&&this.canvasHeight>0&&this.startCropMode()},10)}updateImageOffset(){if(!this.image)return;const t=this.canvasWidth/this.image.naturalWidth,e=this.canvasHeight/this.image.naturalHeight;let a;a=this.autoFit?Math.min(t,e,1):1;const i=this.image.naturalWidth*a,s=this.image.naturalHeight*a;this.imageOffsetX=(this.canvasWidth-i)/2,this.imageOffsetY=(this.canvasHeight-s)/2,this.imageScale=a,console.log("Updated image offset:",this.imageOffsetX,this.imageOffsetY,"scale:",this.imageScale,"autoFit:",this.autoFit)}setCanvasSize(t){super.setCanvasSize(t),this.image&&this.isLoaded&&this.updateImageOffset()}renderImage(){if(!this.image)return;const t=this.canvasWidth/this.image.naturalWidth,e=this.canvasHeight/this.image.naturalHeight;let a;a=this.autoFit?Math.min(t,e,1):1;const i=this.image.naturalWidth*a,s=this.image.naturalHeight*a,n=(this.canvasWidth-i)/2,o=(this.canvasHeight-s)/2;this.context.drawImage(this.image,n,o,i,s)}setupCropListeners(){this.canvas&&(this.canvas.addEventListener("mousedown",t=>this.handleMouseDown(t)),document.addEventListener("mousemove",this._handleMouseMove),document.addEventListener("mouseup",this._handleMouseUp),this.canvas.addEventListener("touchstart",t=>this.handleTouchStart(t),{passive:!1}),this.canvas.addEventListener("touchmove",t=>this.handleTouchMove(t),{passive:!1}),this.canvas.addEventListener("touchend",t=>this.handleTouchEnd(t)),this.canvas.style.cursor="crosshair")}renderCanvas(){super.renderCanvas(),this.cropMode&&this.renderCropOverlay()}renderCropOverlay(){if(!this.cropMode||!this.cropBox)return;const t=this.imageToCanvas(this.cropBox);this.context.save(),this.context.globalAlpha=.5,this.context.fillStyle="#000000",this.context.fillRect(0,0,this.canvasWidth,this.canvasHeight),this.context.globalCompositeOperation="destination-out",this.context.fillRect(t.x,t.y,t.width,t.height),this.context.globalCompositeOperation="source-over",this.context.globalAlpha=1,this.context.strokeStyle="rgba(255, 255, 255, 0.9)",this.context.lineWidth=2,this.context.strokeRect(t.x,t.y,t.width,t.height),this.showGrid&&this.drawGrid(),this.drawHandles(),this.context.restore()}exportImageBlob(t=.9){return this.canvas&&this.image&&this.isLoaded&&this.cropMode?new Promise(e=>{try{console.log("[ImageCropView] Exporting cropped image without overlay"),console.log("[ImageCropView] Crop box:",this.cropBox);const a={x:Math.max(0,Math.min(this.cropBox.x,this.image.naturalWidth)),y:Math.max(0,Math.min(this.cropBox.y,this.image.naturalHeight)),width:Math.min(this.cropBox.width,this.image.naturalWidth-this.cropBox.x),height:Math.min(this.cropBox.height,this.image.naturalHeight-this.cropBox.y)};console.log("[ImageCropView] Crop area in image coords:",a);let i=a.width,s=a.height;this.cropAndScale&&(i=this.cropAndScale.width,s=this.cropAndScale.height,console.log("[ImageCropView] Scaling to:",i,"x",s));const n=document.createElement("canvas");n.width=i,n.height=s,n.getContext("2d").drawImage(this.image,a.x,a.y,a.width,a.height,0,0,i,s),n.toBlob(t=>{console.log("[ImageCropView] Successfully exported cropped image blob:",t?.size,"bytes"),e(t)},"image/png",t)}catch(a){console.error("Failed to export cropped image blob:",a),e(null)}}):super.exportImageBlob(t)}drawGrid(){const t=this.imageToCanvas(this.cropBox);this.context.globalAlpha=.6,this.context.strokeStyle="rgba(255, 255, 255, 0.7)",this.context.lineWidth=1;const e=t.width/3,a=t.height/3;for(let i=1;i<3;i++){const a=t.x+e*i;this.context.beginPath(),this.context.moveTo(a,t.y),this.context.lineTo(a,t.y+t.height),this.context.stroke()}for(let i=1;i<3;i++){const e=t.y+a*i;this.context.beginPath(),this.context.moveTo(t.x,e),this.context.lineTo(t.x+t.width,e),this.context.stroke()}}drawHandles(){if(this.fixedCropSize)return;const t=this.imageToCanvas(this.cropBox);this.context.globalAlpha=1,this.context.fillStyle="#ffffff",this.context.strokeStyle="#000000",this.context.lineWidth=1,Object.keys(this.handles).forEach(e=>{const a=this.handles[e],i=t.x+t.width*a.x,s=t.y+t.height*a.y,n=i-this.handleSize/2,o=s-this.handleSize/2;this.context.fillRect(n,o,this.handleSize,this.handleSize),this.context.strokeRect(n,o,this.handleSize,this.handleSize)})}handleMouseDown(t){if(!this.cropMode)return;t.preventDefault();const e=this.canvas.getBoundingClientRect(),a=t.clientX-e.left,i=t.clientY-e.top,s=this.pointCanvasToImage(a,i);if(this.dragStartImageX=s.x,this.dragStartImageY=s.y,this.initialCropBox={...this.cropBox},this.fixedCropSize)this.isPointInCropBox(a,i)&&(this.isDragging=!0,this.canvas.style.cursor="move");else{const t=this.getHandleAt(a,i);t?(this.isResizing=!0,this.dragHandle=t,this.canvas.style.cursor=this.handles[t].cursor):this.isPointInCropBox(a,i)?(this.isDragging=!0,this.canvas.style.cursor="move"):this.startNewCrop(s.x,s.y)}}handleMouseMove(t){if(!this.cropMode)return;const e=this.canvas.getBoundingClientRect(),a=t.clientX-e.left,i=t.clientY-e.top;this.isResizing&&this.dragHandle?this.resizeCropBox(a,i):this.isDragging?this.moveCropBox(a,i):this.isDragging||this.isResizing||this.updateCursor(a,i)}handleMouseUp(t){if(!this.cropMode)return;this.isDragging=!1,this.isResizing=!1,this.dragHandle=null,this.initialCropBox=null,this.newCropStart=null;const e=this.canvas.getBoundingClientRect(),a=t.clientX-e.left,i=t.clientY-e.top;this.updateCursor(a,i)}handleTouchStart(t){if(!this.cropMode||1!==t.touches.length)return;t.preventDefault();const e=t.touches[0],a=this.canvas.getBoundingClientRect();e.clientX,a.left,e.clientY,a.top,this.handleMouseDown({clientX:e.clientX,clientY:e.clientY,preventDefault:()=>{}})}handleTouchMove(t){if(!this.cropMode||1!==t.touches.length)return;t.preventDefault();const e=t.touches[0];this.handleMouseMove({clientX:e.clientX,clientY:e.clientY})}handleTouchEnd(t){this.cropMode&&this.handleMouseUp({})}getHandleAt(t,e){const a=this.imageToCanvas(this.cropBox);for(const[i,s]of Object.entries(this.handles)){const n=a.x+a.width*s.x,o=a.y+a.height*s.y,r=this.handleSize+4,l=n-r/2,h=o-r/2;if(t>=l&&t<=l+r&&e>=h&&e<=h+r)return i}return null}isPointInCropBox(t,e){const a=this.pointCanvasToImage(t,e);return a.x>=this.cropBox.x&&a.x<=this.cropBox.x+this.cropBox.width&&a.y>=this.cropBox.y&&a.y<=this.cropBox.y+this.cropBox.height}updateCursor(t,e){if(!this.cropMode)return;const a=t,i=e;if(this.fixedCropSize)this.isPointInCropBox(a,i)?this.canvas.style.cursor="move":this.canvas.style.cursor="default";else{const s=this.getHandleAt(t,e);s?this.canvas.style.cursor=this.handles[s].cursor:this.isPointInCropBox(a,i)?this.canvas.style.cursor="move":this.canvas.style.cursor="crosshair"}}startNewCrop(t,e){this.newCropStart={x:t,y:e},this.cropBox={x:t,y:e,width:0,height:0},this.isResizing=!0,this.dragHandle="se"}resizeCropBox(t,e){if(!this.dragHandle)return;const a=this.pointCanvasToImage(t,e);if(this.newCropStart){const t=this.newCropStart.x,e=this.newCropStart.y;return this.cropBox={x:Math.min(t,a.x),y:Math.min(e,a.y),width:Math.abs(a.x-t),height:Math.abs(a.y-e)},this.aspectRatio&&this.constrainToAspectRatio(this.cropBox,"se"),this.cropBox.width<this.minCropSize&&(this.cropBox.width=this.minCropSize),this.cropBox.height<this.minCropSize&&(this.cropBox.height=this.minCropSize),void this.constrainCropBox(this.cropBox)}if(!this.initialCropBox)return;const i=a.x-this.dragStartImageX,s=a.y-this.dragStartImageY;let n={...this.initialCropBox};switch(this.dragHandle){case"nw":n.x+=i,n.y+=s,n.width-=i,n.height-=s;break;case"ne":n.y+=s,n.width+=i,n.height-=s;break;case"sw":n.x+=i,n.width-=i,n.height+=s;break;case"se":n.width+=i,n.height+=s;break;case"n":n.y+=s,n.height-=s;break;case"s":n.height+=s;break;case"w":n.x+=i,n.width-=i;break;case"e":n.width+=i}this.aspectRatio&&this.constrainToAspectRatio(n,this.dragHandle),this.constrainCropBox(n),this.cropBox=n,this.renderCanvas()}moveCropBox(t,e){if(!this.initialCropBox)return;const a=this.pointCanvasToImage(t,e),i=a.x-this.dragStartImageX,s=a.y-this.dragStartImageY;let n={x:this.initialCropBox.x+i,y:this.initialCropBox.y+s,width:this.initialCropBox.width,height:this.initialCropBox.height};this.image&&(n.x=Math.max(0,Math.min(this.image.naturalWidth-n.width,n.x)),n.y=Math.max(0,Math.min(this.image.naturalHeight-n.height,n.y))),this.cropBox=n,this.renderCanvas()}constrainToAspectRatio(t,e){let a,i,s=this.aspectRatio;if(this.cropAndScale&&(s=this.cropAndScale.width/this.cropAndScale.height),s)if(["nw","ne","sw","se"].includes(e)){switch(e){case"nw":a=t.x+t.width,i=t.y+t.height;break;case"ne":a=t.x,i=t.y+t.height;break;case"sw":a=t.x+t.width,i=t.y;break;case"se":a=t.x,i=t.y}switch(t.width/t.height>s?t.width=t.height*s:t.height=t.width/s,e){case"nw":t.x=a-t.width,t.y=i-t.height;break;case"ne":t.x=a,t.y=i-t.height;break;case"sw":t.x=a-t.width,t.y=i;break;case"se":t.x=a,t.y=i}}else if(["n","s"].includes(e)){const e=t.x+t.width/2;t.width=t.height*s,t.x=e-t.width/2}else if(["w","e"].includes(e)){const e=t.y+t.height/2;t.height=t.width/s,t.y=e-t.height/2}}constrainCropBox(t){t.width=Math.max(this.minCropSize,t.width),t.height=Math.max(this.minCropSize,t.height),this.image&&(t.x<0&&(t.width+=t.x,t.x=0),t.y<0&&(t.height+=t.y,t.y=0),t.x+t.width>this.image.naturalWidth&&(t.width=this.image.naturalWidth-t.x),t.y+t.height>this.image.naturalHeight&&(t.height=this.image.naturalHeight-t.y)),t.width=Math.max(0,t.width),t.height=Math.max(0,t.height)}startCropMode(){this.cropMode?console.log("Crop mode already active, skipping initialization"):(this.cropMode=!0,this.initializeCropBox(),console.log("Crop mode started - SE handle should be at buffer coords (100, 100)"),this.renderCanvas(),this.emitCropEvent("crop-started"))}exitCropMode(){this.cropMode=!1,this.isDragging=!1,this.isResizing=!1,this.dragHandle=null,this.canvas.style.cursor="default",this.renderCanvas(),this.emitCropEvent("crop-exited")}initializeCropBox(){if(!this.canvasWidth||!this.canvasHeight||!this.image)return;const t=this.image.naturalWidth,e=this.image.naturalHeight;let a,i;if(this.fixedCropSize)a=this.fixedCropSize.width,i=this.fixedCropSize.height;else{a=Math.floor(.8*t),i=Math.floor(.8*e);let s=this.aspectRatio;this.cropAndScale&&(s=this.cropAndScale.width/this.cropAndScale.height),this.aspectRatio=s,s&&(a/i>s?a=i*s:i=a/s),a=Math.max(this.minCropSize||50,a),i=Math.max(this.minCropSize||50,i)}const s=Math.floor((t-a)/2),n=Math.floor((e-i)/2);this.cropBox={x:s,y:n,width:a,height:i}}setAspectRatio(t){this.aspectRatio=t,this.cropMode&&(this.initializeCropBox(),this.renderCanvas()),this.emitCropEvent("aspect-ratio-changed",{aspectRatio:t})}getCropData(){return this.cropBox&&this.image?{x:Math.max(0,Math.min(this.cropBox.x,this.image.naturalWidth)),y:Math.max(0,Math.min(this.cropBox.y,this.image.naturalHeight)),width:Math.min(this.cropBox.width,this.image.naturalWidth-this.cropBox.x),height:Math.min(this.cropBox.height,this.image.naturalHeight-this.cropBox.y),originalWidth:this.image.naturalWidth,originalHeight:this.image.naturalHeight}:null}async applyCrop(){const t=this.getCropData();if(!t||!this.image)return null;const e=document.createElement("canvas"),a=e.getContext("2d");this.cropAndScale?(e.width=this.cropAndScale.width,e.height=this.cropAndScale.height,a.drawImage(this.image,t.x,t.y,t.width,t.height,0,0,this.cropAndScale.width,this.cropAndScale.height)):(e.width=t.width,e.height=t.height,a.drawImage(this.image,t.x,t.y,t.width,t.height,0,0,t.width,t.height));const i=e.toDataURL("image/png");return{canvas:e,imageData:i,cropData:t}}emitCropEvent(t,e={}){const a=this.getApp()?.events;a&&a.emit(`imagecrop:${t}`,{view:this,cropBox:this.cropBox,aspectRatio:this.aspectRatio,...e})}showToolbarElement(){if(!this.showToolbar){this.showToolbar=!0;const t=this.element.querySelector(".image-crop-toolbar");t&&(t.style.display="block"),this.updateAutoFitButtonState()}}hideToolbarElement(){if(this.showToolbar){this.showToolbar=!1;const t=this.element.querySelector(".image-crop-toolbar");t&&(t.style.display="none")}}toggleToolbarElement(){this.showToolbar?this.hideToolbarElement():this.showToolbarElement()}async onPassThruActionSetAspectRatio(t,e){const a=e.getAttribute("data-ratio"),i="free"===a?null:parseFloat(a);this.setAspectRatio(i)}async handleActionApplyCrop(){if(this.cropMode){const t=await this.applyCrop();t&&t.imageData&&(this.loadImage(t.imageData),this.exitCropMode(),this.emitCropEvent("crop-applied",{result:t}))}}async handleActionToggleAutoFit(){this.showToolbar&&(this.autoFit=!this.autoFit,this.updateAutoFitButtonState(),this.updateImageOffset(),this.renderCanvas(),this.emitCropEvent("auto-fit-changed",{autoFit:this.autoFit}))}async handleActionResetCrop(){this.cropMode&&this.exitCropMode(),this.originalImageUrl&&await this.loadImage(this.originalImageUrl),this.startCropMode(),this.emitCropEvent("crop-reset")}async onBeforeDestroy(){await super.onBeforeDestroy(),this.cropMode=!1,this.isDragging=!1,this.isResizing=!1,document.removeEventListener("mousemove",this._handleMouseMove),document.removeEventListener("mouseup",this._handleMouseUp),this.emitCropEvent("destroyed")}static async showDialog(t,a={}){const{title:i="Crop Image",alt:s="Image",size:n="xl",aspectRatio:o=null,minCropSize:r=50,showGrid:l=!0,showToolbar:h=!1,autoFit:c=!0,fixedCropSize:d=null,cropAndScale:m=null,canvasSize:g=n||"auto",...u}=a,p=new ImageCropView({imageUrl:t,alt:s,title:i,aspectRatio:o,minCropSize:r,canvasSize:g||n||"md",fixedCropSize:d,cropAndScale:m,showGrid:l,showToolbar:h,autoFit:c}),v=new e.default({title:i,body:p,size:n,centered:!0,backdrop:"static",keyboard:!0,noBodyPadding:!0,buttons:[{text:"Cancel",action:"cancel",class:"btn btn-secondary",dismiss:!0},{text:"Apply Crop",action:"apply-crop",class:"btn btn-primary"}],...u});return await v.render(!0,document.body),v.show(),v.on("shown",()=>{if(p.setupCanvas&&p.setupCanvas(),p.isLoaded&&p.canvasWidth>0)p.startCropMode();else{const t=setInterval(()=>{p.isLoaded&&p.canvasWidth>0&&(clearInterval(t),p.startCropMode())},100);setTimeout(()=>clearInterval(t),5e3)}}),new Promise(t=>{v.on("hidden",()=>{v.destroy(),t({action:"cancel",view:p})}),v.on("action:cancel",()=>{v.hide()}),v.on("action:apply-crop",async()=>{let e;if(p.cropMode&&p.cropBox)e=await p.applyCrop();else{const t=p.canvas.toDataURL("image/png");e={canvas:p.canvas,imageData:t,cropData:null}}v.hide(),t({action:"crop",view:p,data:e?.imageData,cropData:e?.cropData})})})}}window.ImageCropView=ImageCropView;class ImageFiltersView extends ImageCanvasView{constructor(t={}){super({...t,className:`image-filters-view ${t.className||""}`}),this.filters={brightness:100,contrast:100,saturation:100,hue:0,blur:0,grayscale:0,sepia:0},this.showControls=t.showControls??!0,this.allowReset=t.allowReset??!0,this.showPresets=t.showPresets??!0,this.showBasicControls=t.showBasicControls??!0,this.showAdvancedControls=t.showAdvancedControls??!0,this.controlsInDropdowns=t.controlsInDropdowns??!0,this.presetEffects={none:{name:"Original",filters:{}},blackWhite:{name:"Black & White",filters:{grayscale:100}},sepia:{name:"Sepia",filters:{sepia:100}},vintage:{name:"Vintage",filters:{sepia:60,contrast:110,brightness:110,saturation:80}},cool:{name:"Cool Tones",filters:{hue:200,saturation:120,brightness:95}},warm:{name:"Warm Tones",filters:{hue:25,saturation:110,brightness:105}},vibrant:{name:"Vibrant",filters:{brightness:105,contrast:115,saturation:140,hue:5}},dramatic:{name:"Dramatic",filters:{brightness:90,contrast:150,saturation:120}},soft:{name:"Soft",filters:{brightness:110,contrast:85,blur:1}}},this.currentPreset="none"}async getTemplate(){return'\n <div class="image-filters-container d-fle