web-mojo 2.4.5 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.es.js +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunks/{AssistantPanelView-C4ZyIQoH.js → AssistantPanelView-CkQEcaFk.js} +2 -2
- package/dist/chunks/{AssistantPanelView-C4ZyIQoH.js.map → AssistantPanelView-CkQEcaFk.js.map} +1 -1
- package/dist/chunks/{AssistantPanelView-DlBgWeM_.js → AssistantPanelView-D1wEbgtM.js} +2 -2
- package/dist/chunks/{AssistantPanelView-DlBgWeM_.js.map → AssistantPanelView-D1wEbgtM.js.map} +1 -1
- package/dist/chunks/ChatView-D2WOSxPu.js +2 -0
- package/dist/chunks/ChatView-D2WOSxPu.js.map +1 -0
- package/dist/chunks/ChatView-kWguc444.js +2 -0
- package/dist/chunks/ChatView-kWguc444.js.map +1 -0
- package/dist/chunks/Passkeys-B1-Z4-16.js +2 -0
- package/dist/chunks/Passkeys-B1-Z4-16.js.map +1 -0
- package/dist/chunks/Passkeys-BlHx11-5.js +2 -0
- package/dist/chunks/Passkeys-BlHx11-5.js.map +1 -0
- package/dist/chunks/{TicketPanelView-DeeANyKv.js → TicketPanelView-DVePzWyJ.js} +2 -2
- package/dist/chunks/{TicketPanelView-DeeANyKv.js.map → TicketPanelView-DVePzWyJ.js.map} +1 -1
- package/dist/chunks/{TicketPanelView-if4ogL_D.js → TicketPanelView-TYh5iZiR.js} +2 -2
- package/dist/chunks/{TicketPanelView-if4ogL_D.js.map → TicketPanelView-TYh5iZiR.js.map} +1 -1
- package/dist/chunks/{UserProfileView-BPjz7uIp.js → UserProfileView-DDflzpTa.js} +2 -2
- package/dist/chunks/{UserProfileView-BPjz7uIp.js.map → UserProfileView-DDflzpTa.js.map} +1 -1
- package/dist/chunks/{UserProfileView-udbSWe1S.js → UserProfileView-cUF8ED9n.js} +2 -2
- package/dist/chunks/{UserProfileView-udbSWe1S.js.map → UserProfileView-cUF8ED9n.js.map} +1 -1
- package/dist/chunks/{admin-C3-HqQvC.js → admin-CHPo4iDn.js} +2 -2
- package/dist/chunks/{admin-C3-HqQvC.js.map → admin-CHPo4iDn.js.map} +1 -1
- package/dist/chunks/{admin-DrFBTXnB.js → admin-CucHFXfD.js} +2 -2
- package/dist/chunks/{admin-DrFBTXnB.js.map → admin-CucHFXfD.js.map} +1 -1
- package/dist/chunks/{index-dFX91Gwz.js → index-Cxffar1o.js} +2 -2
- package/dist/chunks/{index-dFX91Gwz.js.map → index-Cxffar1o.js.map} +1 -1
- package/dist/chunks/{index-Byf1pa3Z.js → index-DsID1QpB.js} +2 -2
- package/dist/chunks/{index-Byf1pa3Z.js.map → index-DsID1QpB.js.map} +1 -1
- package/dist/chunks/{version-C9vC8_Oy.js → version-BYy2UAT0.js} +2 -2
- package/dist/chunks/{version-C9vC8_Oy.js.map → version-BYy2UAT0.js.map} +1 -1
- package/dist/chunks/{version-CjNfmLdK.js → version-BxLEknar.js} +2 -2
- package/dist/chunks/{version-CjNfmLdK.js.map → version-BxLEknar.js.map} +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/dist/user-profile.cjs.js +1 -1
- package/dist/user-profile.es.js +1 -1
- package/dist/web-mojo.lite.iife.js +13 -0
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +44 -44
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/ChatView-CcR1GR30.js +0 -2
- package/dist/chunks/ChatView-CcR1GR30.js.map +0 -1
- package/dist/chunks/ChatView-ChyTcmrd.js +0 -2
- package/dist/chunks/ChatView-ChyTcmrd.js.map +0 -1
- package/dist/chunks/Passkeys-DJn9yy-0.js +0 -2
- package/dist/chunks/Passkeys-DJn9yy-0.js.map +0 -1
- package/dist/chunks/Passkeys-WWAg5tKf.js +0 -2
- package/dist/chunks/Passkeys-WWAg5tKf.js.map +0 -1
package/dist/lightbox.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./chunks/View-CPWwS19u.js"),e=require("./chunks/Modal-Bm1OQ8Ou.js"),i=require("./chunks/WebApp-BdxhRnnT.js"),s=require("./chunks/version-C9vC8_Oy.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" data-container="controls">\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(){if(this.canvas&&this.containerElement){if(this.context&&(this.context.imageSmoothingEnabled=!0,this.context.imageSmoothingQuality="high"),this.resizeCanvas(),!this._sizingPoll&&(!this.canvasWidth||this.canvasWidth<=1||!this.canvasHeight||this.canvasHeight<=1)){const t=Date.now();this._sizingPoll=setInterval(()=>{if(!this.canvas||!this.containerElement)return clearInterval(this._sizingPoll),void(this._sizingPoll=null);this.resizeCanvas(),(this.canvasWidth>1&&this.canvasHeight>1||Date.now()-t>2e3)&&(clearInterval(this._sizingPoll),this._sizingPoll=null)},50)}"undefined"==typeof ResizeObserver||this._resizeObserver||(this._resizeObserver=new ResizeObserver(()=>this.resizeCanvas()),this._resizeObserver.observe(this.containerElement))}}resizeCanvas(){if(!this.canvas||!this.containerElement)return;const t=Math.max(1,Math.floor(this.containerElement.clientWidth)),e=Math.max(1,Math.floor(this.containerElement.clientHeight));if(t===this.canvasWidth&&e===this.canvasHeight)return;const i=window.devicePixelRatio||1;this.canvasWidth=t,this.canvasHeight=e,this.canvas.width=t*i,this.canvas.height=e*i,this.canvas.style.width=t+"px",this.canvas.style.height=e+"px",this.context.setTransform(i,0,0,i,0,0),this.isLoaded&&this.image&&this.autoFit?this.fitToContainer():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"),requestAnimationFrame(()=>{this.canvasWidth&&this.canvasHeight||this.resizeCanvas(),this.canvasWidth&&this.canvasHeight&&(this.autoFit?this.fitToContainer():this.smartFit(),this.renderCanvas(),this.updateControls())});const t=this.getApp()?.events;t&&t.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(),i=t.clientX-e.left,s=t.clientY-e.top,a=i-this.lastPointerX,n=s-this.lastPointerY;this.pan(a,n),this.lastPointerX=i,this.lastPointerY=s}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(),i=t.clientX-e.left,s=t.clientY-e.top,a=t.deltaY>0?.5*-this.scaleStep:.5*this.scaleStep;this.zoomAtPoint(this.scale+a,i,s)}handleTouchStart(t){if(1===t.touches.length&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect();this.isDragging=!0,this.lastPointerX=e.clientX-i.left,this.lastPointerY=e.clientY-i.top}}handleTouchMove(t){if(1===t.touches.length&&this.isDragging&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect(),s=e.clientX-i.left,a=e.clientY-i.top,n=s-this.lastPointerX,o=a-this.lastPointerY;this.pan(n,o),this.lastPointerX=s,this.lastPointerY=a}}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 i=this.getApp()?.events;i&&e!==this.scale&&i.emit("imageviewer:scale-changed",{viewer:this,oldScale:e,newScale:this.scale})}zoomAtPoint(t,e,i){if(!this.image)return;const s=this.scale;if(this.setScale(t),s!==this.scale){const t=this.scale/s,a=this.canvasWidth/2,n=this.canvasHeight/2;this.translateX=(this.translateX-(e-a))*t+(e-a),this.translateY=(this.translateY-(i-n))*t+(i-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 i=this.getApp()?.events;i&&i.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,i=t/this.image.naturalWidth,s=e/this.image.naturalHeight,a=Math.min(i,s,1);this.setScale(a),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,i=Math.min(t,e);i<1&&this.setScale(i),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"]'),i=this.controlsElement.querySelector('[data-action="zoom-out"]');e&&(e.disabled=this.scale>=this.maxScale),i&&(i.disabled=this.scale<=this.minScale)}setImage(t,e="",i=""){const s=this.imageUrl;this.imageUrl=t,this.alt=e,this.title=i,this.reset(),this.loadImage(t);const a=this.getApp()?.events;a&&a.emit("imageviewer:image-changed",{viewer:this,oldImageUrl:s,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),this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),this._sizingPoll&&(clearInterval(this._sizingPoll),this._sizingPoll=null);const t=this.getApp()?.events;t&&t.emit("imageviewer:destroyed",{viewer:this})}static async showDialog(t,i={}){const{title:s="Image Viewer",alt:a="Image",size:n="fullscreen",showControls:o=!0,allowRotate:r=!0,allowZoom:l=!0,allowPan:h=!0,allowDownload:c=!0,...d}=i,m=new ImageViewer({imageUrl:t,alt:a,title:s,showControls:o,allowRotate:r,allowZoom:l,allowPan:h,allowDownload:c,autoFit:!0});return e.Modal.dialog({title:s,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 i,s;if("fullscreen"===t)i=Math.min(1200,.9*window.innerWidth),s=Math.min(900,.8*window.innerHeight);else if("auto"!==t&&e){const t=window.innerWidth*this.maxCanvasWidthPercent,a=window.innerHeight*this.maxCanvasHeightPercent;if(e.width>t||e.height>a)if(this.image){const e=t/this.image.naturalWidth,n=a/this.image.naturalHeight,o=Math.min(e,n,1);i=Math.floor(this.image.naturalWidth*o),s=Math.floor(this.image.naturalHeight*o),i=Math.max(300,i),s=Math.max(200,s)}else i=Math.min(600,t),s=Math.min(450,a);else i=e.width,s=e.height}else if(this.image){const t=window.innerWidth*this.maxCanvasWidthPercent,e=window.innerHeight*this.maxCanvasHeightPercent,a=t/this.image.naturalWidth,n=e/this.image.naturalHeight,o=Math.min(a,n,1);i=Math.floor(this.image.naturalWidth*o),s=Math.floor(this.image.naturalHeight*o),i=Math.max(300,i),s=Math.max(200,s)}else i=Math.min(600,window.innerWidth*this.maxCanvasWidthPercent),s=Math.min(450,window.innerHeight*this.maxCanvasHeightPercent);if(i=Math.min(i,window.innerWidth*this.maxCanvasWidthPercent),s=Math.min(s,window.innerHeight*this.maxCanvasHeightPercent),Math.abs(i-this.canvasWidth)<10&&Math.abs(s-this.canvasHeight)<10)return;const a=window.devicePixelRatio||1;this.canvasWidth=i,this.canvasHeight=s,this.canvas.width=i*a,this.canvas.height=s*a,this.canvas.style.width=i+"px",this.canvas.style.height=s+"px",this.context.setTransform(a,0,0,a,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,i=Math.min(t,e,1),s=this.image.naturalWidth*i,a=this.image.naturalHeight*i,n=(this.canvasWidth-s)/2,o=(this.canvasHeight-a)/2;this.context.drawImage(this.image,n,o,s,a)}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(i){console.error("Failed to export image blob:",i),e(null)}}):Promise.resolve(null)}setImage(t,e="",i=""){const s=this.imageUrl;this.alt=e,this.title=i,this.loadImage(t);const a=this.getApp()?.events;a&&a.emit("imagecanvas:image-changed",{view:this,oldImageUrl:s,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(),i=t.clientX-e.left,s=t.clientY-e.top,a=i-this.lastPointerX,n=s-this.lastPointerY;this.pan(a,n),this.lastPointerX=i,this.lastPointerY=s}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(),i=t.clientX-e.left,s=t.clientY-e.top,a=t.deltaY>0?.5*-this.scaleStep:.5*this.scaleStep;this.zoomAtPoint(this.scale+a,i,s)}handleTouchStart(t){if(1===t.touches.length&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect();this.isDragging=!0,this.lastPointerX=e.clientX-i.left,this.lastPointerY=e.clientY-i.top}}handleTouchMove(t){if(1===t.touches.length&&this.isDragging&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect(),s=e.clientX-i.left,a=e.clientY-i.top,n=s-this.lastPointerX,o=a-this.lastPointerY;this.pan(n,o),this.lastPointerX=s,this.lastPointerY=a}}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,i){if(!this.image)return;const s=this.scale;if(this.setScale(t),s!==this.scale){const t=this.scale/s,a=this.canvasWidth/2,n=this.canvasHeight/2;this.translateX=(this.translateX-(e-a))*t+(e-a),this.translateY=(this.translateY-(i-n))*t+(i-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,i=t/this.image.naturalWidth,s=e/this.image.naturalHeight,a=Math.min(i,s,1);this.setScale(a),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,i=Math.min(t,e);i<1&&this.setScale(i),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 i=this.getApp()?.events;i&&i.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,i={}){const{title:s="Transform Image",alt:a="Image",size:n="xl",allowPan:o=!0,allowZoom:r=!0,allowRotate:l=!0,...h}=i,c=new ImageTransformView({imageUrl:t,alt:a,title:s,allowPan:o,allowZoom:r,allowRotate:l}),d=new e.ModalView({title:s,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,i=this.canvasHeight/this.image.naturalHeight;let s;s=this.autoFit?Math.min(e,i,1):1;const a=this.image.naturalWidth*s,n=this.image.naturalHeight*s,o=(this.canvasWidth-a)/2,r=(this.canvasHeight-n)/2;return{x:t.x*s+o,y:t.y*s+r,width:t.width*s,height:t.height*s}}canvasToImage(t){if(!this.image)return t;const e=this.canvasWidth/this.image.naturalWidth,i=this.canvasHeight/this.image.naturalHeight;let s;s=this.autoFit?Math.min(e,i,1):1;const a=this.image.naturalWidth*s,n=this.image.naturalHeight*s,o=(this.canvasWidth-a)/2,r=(this.canvasHeight-n)/2;return{x:(t.x-o)/s,y:(t.y-r)/s,width:t.width/s,height:t.height/s}}pointCanvasToImage(t,e){const i=this.canvasToImage({x:t,y:e,width:0,height:0});return{x:i.x,y:i.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 i;i=this.autoFit?Math.min(t,e,1):1;const s=this.image.naturalWidth*i,a=this.image.naturalHeight*i;this.imageOffsetX=(this.canvasWidth-s)/2,this.imageOffsetY=(this.canvasHeight-a)/2,this.imageScale=i,this.imageOffsetX,this.imageOffsetY,this.imageScale,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 i;i=this.autoFit?Math.min(t,e,1):1;const s=this.image.naturalWidth*i,a=this.image.naturalHeight*i,n=(this.canvasWidth-s)/2,o=(this.canvasHeight-a)/2;this.context.drawImage(this.image,n,o,s,a)}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{this.cropBox;const i={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)};let s=i.width,a=i.height;this.cropAndScale&&(s=this.cropAndScale.width,a=this.cropAndScale.height);const n=document.createElement("canvas");n.width=s,n.height=a,n.getContext("2d").drawImage(this.image,i.x,i.y,i.width,i.height,0,0,s,a),n.toBlob(t=>{e(t)},"image/png",t)}catch(i){console.error("Failed to export cropped image blob:",i),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,i=t.height/3;for(let s=1;s<3;s++){const i=t.x+e*s;this.context.beginPath(),this.context.moveTo(i,t.y),this.context.lineTo(i,t.y+t.height),this.context.stroke()}for(let s=1;s<3;s++){const e=t.y+i*s;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 i=this.handles[e],s=t.x+t.width*i.x,a=t.y+t.height*i.y,n=s-this.handleSize/2,o=a-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(),i=t.clientX-e.left,s=t.clientY-e.top,a=this.pointCanvasToImage(i,s);if(this.dragStartImageX=a.x,this.dragStartImageY=a.y,this.initialCropBox={...this.cropBox},this.fixedCropSize)this.isPointInCropBox(i,s)&&(this.isDragging=!0,this.canvas.style.cursor="move");else{const t=this.getHandleAt(i,s);t?(this.isResizing=!0,this.dragHandle=t,this.canvas.style.cursor=this.handles[t].cursor):this.isPointInCropBox(i,s)?(this.isDragging=!0,this.canvas.style.cursor="move"):this.startNewCrop(a.x,a.y)}}handleMouseMove(t){if(!this.cropMode)return;const e=this.canvas.getBoundingClientRect(),i=t.clientX-e.left,s=t.clientY-e.top;this.isResizing&&this.dragHandle?this.resizeCropBox(i,s):this.isDragging?this.moveCropBox(i,s):this.isDragging||this.isResizing||this.updateCursor(i,s)}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(),i=t.clientX-e.left,s=t.clientY-e.top;this.updateCursor(i,s)}handleTouchStart(t){if(!this.cropMode||1!==t.touches.length)return;t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect();e.clientX,i.left,e.clientY,i.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 i=this.imageToCanvas(this.cropBox);for(const[s,a]of Object.entries(this.handles)){const n=i.x+i.width*a.x,o=i.y+i.height*a.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 s}return null}isPointInCropBox(t,e){const i=this.pointCanvasToImage(t,e);return i.x>=this.cropBox.x&&i.x<=this.cropBox.x+this.cropBox.width&&i.y>=this.cropBox.y&&i.y<=this.cropBox.y+this.cropBox.height}updateCursor(t,e){if(!this.cropMode)return;const i=t,s=e;if(this.fixedCropSize)this.isPointInCropBox(i,s)?this.canvas.style.cursor="move":this.canvas.style.cursor="default";else{const a=this.getHandleAt(t,e);a?this.canvas.style.cursor=this.handles[a].cursor:this.isPointInCropBox(i,s)?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 i=this.pointCanvasToImage(t,e);if(this.newCropStart){const t=this.newCropStart.x,e=this.newCropStart.y;return this.cropBox={x:Math.min(t,i.x),y:Math.min(e,i.y),width:Math.abs(i.x-t),height:Math.abs(i.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 s=i.x-this.dragStartImageX,a=i.y-this.dragStartImageY;let n={...this.initialCropBox};switch(this.dragHandle){case"nw":n.x+=s,n.y+=a,n.width-=s,n.height-=a;break;case"ne":n.y+=a,n.width+=s,n.height-=a;break;case"sw":n.x+=s,n.width-=s,n.height+=a;break;case"se":n.width+=s,n.height+=a;break;case"n":n.y+=a,n.height-=a;break;case"s":n.height+=a;break;case"w":n.x+=s,n.width-=s;break;case"e":n.width+=s}this.aspectRatio&&this.constrainToAspectRatio(n,this.dragHandle),this.constrainCropBox(n),this.cropBox=n,this.renderCanvas()}moveCropBox(t,e){if(!this.initialCropBox)return;const i=this.pointCanvasToImage(t,e),s=i.x-this.dragStartImageX,a=i.y-this.dragStartImageY;let n={x:this.initialCropBox.x+s,y:this.initialCropBox.y+a,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 i,s,a=this.aspectRatio;if(this.cropAndScale&&(a=this.cropAndScale.width/this.cropAndScale.height),a)if(["nw","ne","sw","se"].includes(e)){switch(e){case"nw":i=t.x+t.width,s=t.y+t.height;break;case"ne":i=t.x,s=t.y+t.height;break;case"sw":i=t.x+t.width,s=t.y;break;case"se":i=t.x,s=t.y}switch(t.width/t.height>a?t.width=t.height*a:t.height=t.width/a,e){case"nw":t.x=i-t.width,t.y=s-t.height;break;case"ne":t.x=i,t.y=s-t.height;break;case"sw":t.x=i-t.width,t.y=s;break;case"se":t.x=i,t.y=s}}else if(["n","s"].includes(e)){const e=t.x+t.width/2;t.width=t.height*a,t.x=e-t.width/2}else if(["w","e"].includes(e)){const e=t.y+t.height/2;t.height=t.width/a,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||(this.cropMode=!0,this.initializeCropBox(),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 i,s;if(this.fixedCropSize)i=this.fixedCropSize.width,s=this.fixedCropSize.height;else{i=Math.floor(.8*t),s=Math.floor(.8*e);let a=this.aspectRatio;this.cropAndScale&&(a=this.cropAndScale.width/this.cropAndScale.height),this.aspectRatio=a,a&&(i/s>a?i=s*a:s=i/a),i=Math.max(this.minCropSize||50,i),s=Math.max(this.minCropSize||50,s)}const a=Math.floor((t-i)/2),n=Math.floor((e-s)/2);this.cropBox={x:a,y:n,width:i,height:s}}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"),i=e.getContext("2d");this.cropAndScale?(e.width=this.cropAndScale.width,e.height=this.cropAndScale.height,i.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,i.drawImage(this.image,t.x,t.y,t.width,t.height,0,0,t.width,t.height));const s=e.toDataURL("image/png");return{canvas:e,imageData:s,cropData:t}}emitCropEvent(t,e={}){const i=this.getApp()?.events;i&&i.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 i=e.getAttribute("data-ratio"),s="free"===i?null:parseFloat(i);this.setAspectRatio(s)}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,i={}){const{title:s="Crop Image",alt:a="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}=i,p=new ImageCropView({imageUrl:t,alt:a,title:s,aspectRatio:o,minCropSize:r,canvasSize:g||n||"md",fixedCropSize:d,cropAndScale:m,showGrid:l,showToolbar:h,autoFit:c}),v=new e.ModalView({title:s,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-flex flex-column h-100">\n {{#showControls}}\n \x3c!-- Filter Toolbar --\x3e\n <div class="image-filters-toolbar bg-light border-bottom p-2">\n <div class="btn-toolbar justify-content-center flex-wrap" role="toolbar">\n\n {{#showPresets}}\n \x3c!-- Preset Effects --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Preset effects">\n <div class="dropdown">\n <button type="button" class="btn btn-outline-primary btn-sm dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false" title="Preset Effects">\n <i class="bi bi-palette"></i> Effects\n </button>\n <ul class="dropdown-menu">\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="none">Original</a></li>\n <li><hr class="dropdown-divider"></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="blackWhite">Black & White</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="sepia">Sepia</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="vintage">Vintage</a></li>\n <li><hr class="dropdown-divider"></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="cool">Cool Tones</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="warm">Warm Tones</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="vibrant">Vibrant</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="dramatic">Dramatic</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="soft">Soft</a></li>\n </ul>\n </div>\n </div>\n {{/showPresets}}\n\n {{#showBasicControls}}\n {{#controlsInDropdowns}}\n \x3c!-- Basic Controls in Dropdown --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Basic controls">\n <div class="dropdown">\n <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false" title="Basic Adjustments">\n <i class="bi bi-sliders"></i> Basic\n </button>\n <div class="dropdown-menu p-3" style="min-width: 300px;">\n <div class="mb-3">\n <label class="form-label small fw-bold">Brightness</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.brightness}}"\n data-change-action="filter-change" data-filter="brightness">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="brightness">{{filters.brightness}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label small fw-bold">Contrast</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.contrast}}"\n data-change-action="filter-change" data-filter="contrast">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="contrast">{{filters.contrast}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="mb-0">\n <label class="form-label small fw-bold">Saturation</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.saturation}}"\n data-change-action="filter-change" data-filter="saturation">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="saturation">{{filters.saturation}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/controlsInDropdowns}}\n {{/showBasicControls}}\n\n {{#showAdvancedControls}}\n {{#controlsInDropdowns}}\n \x3c!-- Advanced Controls in Dropdown --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Advanced controls">\n <div class="dropdown">\n <button type="button" class="btn btn-outline-warning btn-sm dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false" title="Advanced Adjustments">\n <i class="bi bi-gear"></i> Advanced\n </button>\n <div class="dropdown-menu p-3" style="min-width: 300px;">\n <div class="mb-3">\n <label class="form-label small fw-bold">Hue</label>\n <input type="range" class="form-range"\n min="0" max="360" value="{{filters.hue}}"\n data-change-action="filter-change" data-filter="hue">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0°</small>\n <small class="text-muted filter-value" data-filter="hue">{{filters.hue}}°</small>\n <small class="text-muted">360°</small>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label small fw-bold">Blur</label>\n <input type="range" class="form-range"\n min="0" max="10" value="{{filters.blur}}"\n data-change-action="filter-change" data-filter="blur">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0px</small>\n <small class="text-muted filter-value" data-filter="blur">{{filters.blur}}px</small>\n <small class="text-muted">10px</small>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label small fw-bold">Grayscale</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.grayscale}}"\n data-change-action="filter-change" data-filter="grayscale">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="grayscale">{{filters.grayscale}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n <div class="mb-0">\n <label class="form-label small fw-bold">Sepia</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.sepia}}"\n data-change-action="filter-change" data-filter="sepia">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="sepia">{{filters.sepia}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/controlsInDropdowns}}\n {{/showAdvancedControls}}\n\n {{#allowReset}}\n \x3c!-- Reset & Preview Controls --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Reset controls">\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="reset-filters" title="Reset All Filters">\n <i class="bi bi-arrow-repeat"></i> Reset\n </button>\n <button type="button" class="btn btn-outline-info btn-sm" data-action="preview-original" title="Preview Original"\n onmousedown="this.dataset.previewing=\'true\'"\n onmouseup="this.dataset.previewing=\'false\'"\n onmouseleave="this.dataset.previewing=\'false\'">\n <i class="bi bi-eye"></i> Original\n </button>\n </div>\n {{/allowReset}}\n\n </div>\n </div>\n {{/showControls}}\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-filters-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\n {{#showControls}}\n {{^controlsInDropdowns}}\n \x3c!-- Expanded Controls Panel (when not in dropdowns) --\x3e\n <div class="image-filters-controls bg-light border-top p-3" data-container="controls" style="max-height: 300px; overflow-y: auto;">\n <div class="row g-3">\n {{#showBasicControls}}\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Brightness</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.brightness}}"\n data-change-action="filter-change" data-filter="brightness">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="brightness">{{filters.brightness}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Contrast</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.contrast}}"\n data-change-action="filter-change" data-filter="contrast">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="contrast">{{filters.contrast}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Saturation</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.saturation}}"\n data-change-action="filter-change" data-filter="saturation">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="saturation">{{filters.saturation}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n {{/showBasicControls}}\n\n {{#showAdvancedControls}}\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Hue</label>\n <input type="range" class="form-range"\n min="0" max="360" value="{{filters.hue}}"\n data-change-action="filter-change" data-filter="hue">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0°</small>\n <small class="text-muted filter-value" data-filter="hue">{{filters.hue}}°</small>\n <small class="text-muted">360°</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Blur</label>\n <input type="range" class="form-range"\n min="0" max="10" value="{{filters.blur}}"\n data-change-action="filter-change" data-filter="blur">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0px</small>\n <small class="text-muted filter-value" data-filter="blur">{{filters.blur}}px</small>\n <small class="text-muted">10px</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Grayscale</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.grayscale}}"\n data-change-action="filter-change" data-filter="grayscale">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="grayscale">{{filters.grayscale}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Sepia</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.sepia}}"\n data-change-action="filter-change" data-filter="sepia">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="sepia">{{filters.sepia}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n {{/showAdvancedControls}}\n </div>\n </div>\n {{/controlsInDropdowns}}\n {{/showControls}}\n </div>\n '}async onAfterRender(){await super.onAfterRender(),this.controlsElement=this.element.querySelector(".image-filters-controls")}setupCanvas(){this.canvas&&this.containerElement&&(this.setCanvasSize(this.canvasSize),"fullscreen"!==this.canvasSize&&"auto"!==this.canvasSize||(this._resizeHandler=()=>this.setCanvasSize(this.canvasSize),window.addEventListener("resize",this._resizeHandler)),this.context.imageSmoothingEnabled=!0,this.context.imageSmoothingQuality="high")}setCanvasSize(t){if(this.canvas&&this.containerElement){if("auto"===t){const t=this.containerElement;let e=t.clientWidth-40,i=t.clientHeight-40;if(e<=40||i<=40){let s=t.parentElement;for(;s&&(s.clientWidth<=40||s.clientHeight<=40)&&(s=s.parentElement,!s||!(s.classList.contains("modal-body")||s.classList.contains("card-body")||s.classList.contains("dialog-body")||"MAIN"===s.tagName||"BODY"===s.tagName)););s&&(e=s.clientWidth-80,i=s.clientHeight-80)}if(e>100&&i>100){let t,s;if(this.image){const a=this.image.naturalWidth/this.image.naturalHeight;a>e/i?(t=e,s=e/a):(s=i,t=i*a),t=Math.max(300,Math.floor(t)),s=Math.max(200,Math.floor(s))}else t=Math.min(600,Math.max(300,e)),s=Math.min(450,Math.max(200,i));return void this.applyCanvasSize(t,s)}}super.setCanvasSize(t)}}applyCanvasSize(t,e){if(Math.abs(t-this.canvasWidth)<10&&Math.abs(e-this.canvasHeight)<10)return;const i=window.devicePixelRatio||1;this.canvasWidth=t,this.canvasHeight=e,this.canvas.width=t*i,this.canvas.height=e*i,this.canvas.style.width=t+"px",this.canvas.style.height=e+"px",this.context.setTransform(i,0,0,i,0,0),this.isLoaded&&this.renderCanvas()}async loadImage(t){await super.loadImage(t),this.renderCanvas()}renderImage(){if(!this.image)return;this.context.filter=this.getFilterString();const t=Math.min(this.canvasWidth/this.image.naturalWidth,this.canvasHeight/this.image.naturalHeight),e=this.image.naturalWidth*t,i=this.image.naturalHeight*t,s=(this.canvasWidth-e)/2,a=(this.canvasHeight-i)/2;this.context.drawImage(this.image,s,a,e,i),this.context.filter="none"}getCombinedFilters(){const t={...this.filters};if("none"!==this.currentPreset&&this.presetEffects[this.currentPreset]){const e=this.presetEffects[this.currentPreset].filters;e&&Object.assign(t,e)}return t}getFilterString(){const t=this.getCombinedFilters();return this.hasFilters()||"none"!==this.currentPreset?[`brightness(${t.brightness}%)`,`contrast(${t.contrast}%)`,`saturate(${t.saturation}%)`,`hue-rotate(${t.hue}deg)`,`blur(${t.blur}px)`,`grayscale(${t.grayscale}%)`,`sepia(${t.sepia}%)`].join(" "):"none"}hasFilters(){return 100!==this.filters.brightness||100!==this.filters.contrast||100!==this.filters.saturation||0!==this.filters.hue||0!==this.filters.blur||0!==this.filters.grayscale||0!==this.filters.sepia}async onPassThruActionResetFilters(){this.resetFilters()}async onPassThruActionApplyPreset(t,e){t.preventDefault();const i=e.getAttribute("data-preset");i&&this.presetEffects[i]&&this.applyPreset(i)}async onPassThruActionPreviewOriginal(t,e){if("true"===e.dataset.previewing){this.context.clearRect(0,0,this.canvasWidth,this.canvasHeight),this.context.filter="none";const t=(this.canvasWidth-this.image.naturalWidth)/2,e=(this.canvasHeight-this.image.naturalHeight)/2;this.context.drawImage(this.image,t,e)}else this.renderCanvas()}async onChangeFilterChange(t,e){const i=e.getAttribute("data-filter"),s=parseFloat(e.value);this.updateFilter(i,s)}updateFilter(t,e){if(!(t in this.filters))return;const i=this.filters[t];this.filters[t]=e,this.updateFilterDisplay(t,e),this.renderCanvas(),this.emitFilterEvent("filter-changed",{filter:t,oldValue:i,newValue:e,allFilters:{...this.filters}})}updateFilterDisplay(t,e){const i=this.element.querySelector(`[data-filter="${t}"].filter-value`);if(i){const s="hue"===t?"°":"blur"===t?"px":"%";i.textContent=`${e}${s}`}}resetFilters(){const t={...this.filters},e=this.currentPreset;this.filters={brightness:100,contrast:100,saturation:100,hue:0,blur:0,grayscale:0,sepia:0},this.currentPreset="none",this.updateAllFilterInputs(),this.renderCanvas(),this.emitFilterEvent("filters-reset",{oldFilters:t,newFilters:{...this.filters},oldPreset:e,newPreset:this.currentPreset})}updateAllFilterInputs(){Object.keys(this.filters).forEach(t=>{const e=this.element.querySelector(`[data-filter="${t}"][type="range"]`);e&&(e.value=this.filters[t],this.updateFilterDisplay(t,this.filters[t]))})}applyPreset(t){this.presetEffects[t]&&(this.presetEffects[t],this.currentPreset=t,this.currentPreset=t,this.renderCanvas(),this.emitFilterEvent("preset-applied",{preset:t,filters:{...this.filters}}))}getFilterState(){return{...this.filters}}setFilterState(t){this.filters={...this.filters,...t},this.updateAllFilterInputs(),this.renderCanvas(),this.emitFilterEvent("filters-set",{filters:{...this.filters}})}exportFilteredImageData(){return this.canvas?this.exportImageData():null}async exportFilteredImageBlob(t=.9){return this.canvas?this.exportImageBlob(t):null}createFilteredCanvas(){if(!this.image)return null;const t=document.createElement("canvas"),e=t.getContext("2d");return t.width=this.image.naturalWidth,t.height=this.image.naturalHeight,e.filter=this.getFilterString(),e.drawImage(this.image,0,0),e.filter="none",t}emitFilterEvent(t,e={}){const i=this.getApp()?.events;i&&i.emit(`imagefilters:${t}`,{view:this,hasFilters:this.hasFilters(),filterString:this.getFilterString(),...e})}handleImageLoad(){super.handleImageLoad(),this.emitFilterEvent("ready",{filters:{...this.filters}})}async onBeforeDestroy(){await super.onBeforeDestroy(),this._resizeHandler&&window.removeEventListener("resize",this._resizeHandler),this.emitFilterEvent("destroyed")}static async showDialog(t,i={}){const{title:s="Apply Filters",alt:a="Image",size:n="xl",showControls:o=!0,allowReset:r=!0,showPresets:l=!0,showBasicControls:h=!0,showAdvancedControls:c=!0,controlsInDropdowns:d=!0,canvasSize:m="auto",autoFit:g=!0,crossOrigin:u="anonymous",...p}=i,v=new ImageFiltersView({imageUrl:t,alt:a,title:s,canvasSize:m,autoFit:g,crossOrigin:u,showControls:o,allowReset:r,showPresets:l,showBasicControls:h,showAdvancedControls:c,controlsInDropdowns:d}),w=new e.ModalView({title:s,body:v,size:n,centered:!0,backdrop:"static",keyboard:!0,noBodyPadding:!0,buttons:[{text:"Cancel",action:"cancel",class:"btn btn-secondary",dismiss:!0},{text:"Apply Filters",action:"apply-filters",class:"btn btn-primary"}],...p});return await w.render(!0,document.body),w.show(),new Promise(t=>{w.on("hidden",()=>{w.destroy(),t({action:"cancel",view:v})}),w.on("action:cancel",()=>{w.hide()}),w.on("action:apply-filters",async()=>{const e=v.exportFilteredImageData();w.hide(),t({action:"filters",view:v,data:e,filterState:v.getFilterState()})})})}}window.ImageFiltersView=ImageFiltersView;class ImageEditor extends t.View{constructor(t={}){super({...t,className:`image-editor ${t.className||""}`,tagName:"div"}),this.imageUrl=t.imageUrl||t.src||"",this.alt=t.alt||"Image",this.title=t.title||"",this.currentImageData=null,this.currentMode=t.startMode||"transform",this.history=[],this.historyIndex=-1,this.maxHistory=t.maxHistory||20,this.showToolbar=!1!==t.showToolbar,this.allowTransform=!1!==t.allowTransform,this.allowCrop=!1!==t.allowCrop,this.allowFilters=!1!==t.allowFilters,this.allowExport=!1!==t.allowExport,this.allowHistory=!1!==t.allowHistory,this.cropOptions=t.cropOptions||{},this.currentView=null,this.isInitialized=!1}async getTemplate(){return'\n <div class="image-editor-container d-flex flex-column h-100">\n {{#showToolbar}}\n \x3c!-- Toolbar --\x3e\n <div class="image-editor-toolbar bg-light border-bottom p-3" data-container="toolbar">\n <div class="d-flex justify-content-between align-items-center">\n \x3c!-- Mode Buttons --\x3e\n <div class="btn-group" role="group" aria-label="Editing modes">\n {{#allowTransform}}\n <button type="button" class="btn btn-outline-primary mode-btn"\n data-action="switch-mode" data-mode="transform"\n title="Transform: Zoom, Pan, Rotate">\n <i class="bi bi-arrows-move"></i> Transform\n </button>\n {{/allowTransform}}\n\n {{#allowCrop}}\n <button type="button" class="btn btn-outline-primary mode-btn"\n data-action="switch-mode" data-mode="crop"\n title="Crop: Select and crop image">\n <i class="bi bi-crop"></i> Crop\n </button>\n {{/allowCrop}}\n\n {{#allowFilters}}\n <button type="button" class="btn btn-outline-primary mode-btn"\n data-action="switch-mode" data-mode="filters"\n title="Filters: Brightness, Contrast, Effects">\n <i class="bi bi-palette"></i> Filters\n </button>\n {{/allowFilters}}\n </div>\n\n \x3c!-- Action Buttons --\x3e\n <div class="btn-group" role="group" aria-label="Actions">\n {{#allowHistory}}\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="undo" title="Undo" disabled>\n <i class="bi bi-arrow-counterclockwise"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="redo" title="Redo" disabled>\n <i class="bi bi-arrow-clockwise"></i>\n </button>\n {{/allowHistory}}\n\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="reset" title="Reset All Changes">\n <i class="bi bi-arrow-repeat"></i>\n </button>\n\n {{#allowExport}}\n <button type="button" class="btn btn-success btn-sm"\n data-action="export" title="Export Image">\n <i class="bi bi-download"></i> Export\n </button>\n {{/allowExport}}\n </div>\n </div>\n\n\n </div>\n {{/showToolbar}}\n\n \x3c!-- Main editing area where child views will be mounted --\x3e\n <div class="image-editor-workspace flex-grow-1 position-relative" data-container="image-workspace">\n \x3c!-- Child views will be added here dynamically --\x3e\n </div>\n\n \x3c!-- Status bar --\x3e\n <div class="image-editor-status bg-light border-top p-2" data-container="status">\n <div class="d-flex justify-content-between align-items-center">\n <small class="text-muted">\n Mode: <span class="current-mode fw-bold">Transform</span>\n </small>\n <small class="text-muted">\n <span class="image-info">Ready</span>\n </small>\n </div>\n </div>\n </div>\n '}async onAfterRender(){this.toolbarElement=this.element.querySelector(".image-editor-toolbar"),this.workspaceElement=this.element.querySelector(".image-editor-workspace"),this.statusElement=this.element.querySelector(".image-editor-status"),this.setupChildViewEvents(),await this.switchMode(this.currentMode,!0),this.saveState(),this.isInitialized=!0}createChildView(t){const e=this.currentImageData||this.imageUrl;this.currentImageData;const i={parent:this,containerId:"image-workspace",imageUrl:e,alt:this.alt,title:this.title};switch(t){case"transform":return this.allowTransform?new ImageTransformView({...i,allowPan:!0,allowZoom:!0,allowRotate:!0}):null;case"crop":return this.allowCrop?new ImageCropView({...i,showGrid:!0,minCropSize:50,...this.cropOptions}):null;case"filters":return this.allowFilters?new ImageFiltersView({...i,showControls:!0,allowReset:!0}):null;default:return null}}setupChildViewEvents(){const t=this.getApp()?.events;t&&(t.on("imagetransform:scale-changed",()=>this.saveState()),t.on("imagetransform:rotated",()=>this.saveState()),t.on("imagetransform:reset",()=>this.saveState()),t.on("imagecrop:crop-applied",t=>{const e=this.getCurrentImageData();e&&(this.currentImageData=e),this.saveState(),this.updateStatus("Crop applied successfully"),this.updateHistoryButtons()}),t.on("imagefilters:filter-changed",()=>{this.saveState(),this.updateStatus("Filter applied")}),t.on("imagefilters:filters-reset",()=>{this.saveState(),this.updateStatus("Filters reset")}))}async handleActionSwitchMode(t,e){const i=e.getAttribute("data-mode");await this.switchMode(i)}async handleActionUndo(){this.undo()}async handleActionRedo(){this.redo()}async handleActionReset(){await this.resetAll()}async handleActionExport(){await this.exportImage()&&this.updateStatus("Image exported successfully")}async switchMode(t,e=!1){if(t===this.currentMode&&!e)return;if(this.currentView&&!e){const t=this.getCurrentImageData();t?(this.currentMode,this.currentImageData=t):this.currentMode}this.currentView&&(await this.currentView.destroy(),this.currentView=null),this.element.querySelectorAll(".mode-btn").forEach(e=>{e.classList.remove("active"),e.getAttribute("data-mode")===t&&e.classList.add("active")}),this.currentMode=t,this.currentView=this.createChildView(t),this.currentView&&(await this.currentView.render(),"crop"===t&&this.currentView.startCropMode?(this.currentView.startCropMode(),this.updateStatus("Click and drag to select crop area")):"transform"===t?this.updateStatus("Use controls to transform the image"):"filters"===t&&this.updateStatus("Adjust filters to enhance the image")),this.updateCurrentModeDisplay();const i=this.getApp()?.events;i&&i.emit("imageeditor:mode-changed",{editor:this,mode:t,currentView:this.currentView})}updateCurrentModeDisplay(){const t=this.element.querySelector(".current-mode");t&&(t.textContent=this.currentMode.charAt(0).toUpperCase()+this.currentMode.slice(1))}updateStatus(t){const e=this.element.querySelector(".image-info");e&&(e.textContent=t)}saveState(){if(!this.isInitialized)return;const t={mode:this.currentMode,transform:this.currentView?.getTransformState?.(),filters:this.currentView?.getFilterState?.(),imageData:this.currentImageData,timestamp:Date.now()};this.history=this.history.slice(0,this.historyIndex+1),this.history.push(t),this.historyIndex=this.history.length-1,this.history.length>this.maxHistory&&(this.history.shift(),this.historyIndex--),this.updateHistoryButtons()}undo(){this.historyIndex>0&&(this.historyIndex--,this.restoreState(this.history[this.historyIndex]))}redo(){this.historyIndex<this.history.length-1&&(this.historyIndex++,this.restoreState(this.history[this.historyIndex]))}async restoreState(t){t.imageData&&(this.currentImageData=t.imageData),await this.switchMode(t.mode,!0),this.currentView&&(t.transform&&this.currentView.setTransformState&&this.currentView.setTransformState(t.transform),t.filters&&this.currentView.setFilterState&&this.currentView.setFilterState(t.filters)),this.updateHistoryButtons(),this.updateStatus(`Restored to ${t.mode} mode`)}updateHistoryButtons(){const t=this.element.querySelector('[data-action="undo"]'),e=this.element.querySelector('[data-action="redo"]');t&&(t.disabled=this.historyIndex<=0),e&&(e.disabled=this.historyIndex>=this.history.length-1)}async resetAll(){this.currentImageData=null,this.currentView&&this.currentView.reset&&this.currentView.reset(),await this.switchMode("transform",!0),this.history=[],this.historyIndex=-1,this.saveState(),this.updateStatus("All changes reset")}async exportImage(t={}){if(!this.currentView)return null;try{let e=null;if(e=this.getCurrentImageData(),e){const i=this.getExportFilename();if(!1!==t.download){const t=document.createElement("a");t.download=i,t.href=e,document.body.appendChild(t),t.click(),document.body.removeChild(t)}const s=this.getApp()?.events;return s&&s.emit("imageeditor:exported",{editor:this,imageData:e,filename:i}),{imageData:e,filename:i}}}catch(e){console.error("Export failed:",e),this.updateStatus("Export failed")}return null}getExportFilename(){return`edited-image-${/* @__PURE__ */(new Date).toISOString().slice(0,19).replace(/[:\-]/g,"")}.png`}async setImage(t,e="",i=""){this.imageUrl=t,this.alt=e,this.title=i,this.currentImageData=null,this.currentView&&this.currentView.setImage&&this.currentView.setImage(t,e,i),await this.resetAll()}getCurrentImageData(){if(!this.currentView)return null;let t=null;return this.currentView.exportImageData?t=this.currentView.exportImageData():this.currentView.exportFilteredImageData&&(t=this.currentView.exportFilteredImageData()),t||null}async onBeforeDestroy(){this.currentView&&(await this.currentView.destroy(),this.currentView=null);const t=this.getApp()?.events;t&&t.emit("imageeditor:destroyed",{editor:this})}static async showDialog(t,i={}){const{title:s="Image Editor",alt:a="Image",size:n="fullscreen",showToolbar:o=!0,allowTransform:r=!0,allowCrop:l=!0,allowFilters:h=!0,allowExport:c=!0,startMode:d="transform",download:m=!0,saveText:g="Export & Close",cropOptions:u,...p}=i,v=new ImageEditor({imageUrl:t,alt:a,title:s,showToolbar:o,allowTransform:r,allowCrop:l,allowFilters:h,allowExport:c,startMode:d,cropOptions:u}),w=new e.ModalView({title:s,body:v,size:n,centered:!0,backdrop:"static",keyboard:!0,buttons:[{text:"Cancel",action:"cancel",class:"btn btn-secondary",dismiss:!0},{text:g,action:"export-close",class:"btn btn-primary"}],...p});return await w.render(!0,document.body),w.show(),new Promise(t=>{let e=!1;w.on("action:cancel",()=>{w.hide()}),w.on("action:export-close",async()=>{const i=await v.exportImage({download:m});e=!0,w.hide(),t({action:"export",editor:v,data:i?.imageData,filename:i?.filename})}),w.on("hidden",()=>{w.destroy(),e||t({action:"cancel",editor:v})})})}}window.ImageEditor=ImageEditor;class ImageUploadView extends t.View{constructor(t={}){super({...t,className:`image-upload-view ${t.className||""}`,tagName:"div"}),this.autoUpload=t.autoUpload||!1,this.acceptedTypes=t.acceptedTypes||["image/jpeg","image/png","image/gif","image/webp"],this.maxFileSize=t.maxFileSize||10485760,this.uploadUrl=t.uploadUrl||null,this.onUpload=t.onUpload||null,this.selectedFile=null,this.isUploading=!1,this.previewUrl=null,this._handleDragOver=this.handleDragOver.bind(this),this._handleDragLeave=this.handleDragLeave.bind(this),this._handleDrop=this.handleDrop.bind(this),this._handleFileSelect=this.handleFileSelect.bind(this),this._preventDefaults=this.preventDefaults.bind(this)}async getTemplate(){return`\n <div class="image-upload-container">\n \x3c!-- Drop Zone --\x3e\n <div class="upload-drop-zone border-2 border-dashed rounded p-4 text-center position-relative" \n style="border-color: #dee2e6; min-height: 200px; transition: all 0.2s ease;">\n \n \x3c!-- Default State --\x3e\n <div class="upload-prompt">\n <i class="bi bi-cloud-upload text-muted" style="font-size: 3rem;"></i>\n <h5 class="mt-3 text-muted">Drop your image here</h5>\n <p class="text-muted mb-3">or</p>\n <button type="button" class="btn btn-outline-primary" data-action="select-file">\n <i class="bi bi-folder2-open"></i> Choose File\n </button>\n <input type="file" class="upload-file-input d-none" accept="image/*" multiple="false">\n <div class="mt-3">\n <small class="text-muted">Supported: JPEG, PNG, GIF, WebP (max ${Math.round(this.maxFileSize/1024/1024)}MB)</small>\n </div>\n </div>\n \n \x3c!-- Preview State --\x3e\n <div class="upload-preview d-none">\n <div class="preview-image-container mb-3">\n <img class="preview-image img-fluid rounded shadow-sm" style="max-height: 300px; max-width: 100%;">\n </div>\n <div class="preview-info">\n <div class="file-name fw-bold mb-2 text-truncate"></div>\n <div class="file-details text-muted small mb-3"></div>\n <div class="upload-actions">\n {{#autoUpload}}\n <button type="button" class="btn btn-outline-secondary" data-action="clear">\n <i class="bi bi-x"></i> Clear\n </button>\n {{/autoUpload}}\n {{^autoUpload}}\n <button type="button" class="btn btn-success me-2" data-action="upload">\n <i class="bi bi-cloud-arrow-up"></i> Upload\n </button>\n <button type="button" class="btn btn-outline-secondary" data-action="clear">\n <i class="bi bi-x"></i> Clear\n </button>\n {{/autoUpload}}\n </div>\n </div>\n </div>\n \n \x3c!-- Loading State --\x3e\n <div class="upload-loading d-none">\n <div class="spinner-border text-primary mb-3" role="status">\n <span class="visually-hidden">Uploading...</span>\n </div>\n <div class="upload-progress">\n <div class="progress mb-2" style="height: 8px;">\n <div class="progress-bar progress-bar-striped progress-bar-animated" \n role="progressbar" style="width: 0%"></div>\n </div>\n <small class="text-muted upload-status">Uploading...</small>\n </div>\n </div>\n </div>\n \n \x3c!-- Upload Result --\x3e\n <div class="upload-result mt-3 d-none">\n <div class="alert" role="alert"></div>\n </div>\n </div>\n `}async onAfterRender(){this.dropZone=this.element.querySelector(".upload-drop-zone"),this.fileInput=this.element.querySelector(".upload-file-input"),this.promptElement=this.element.querySelector(".upload-prompt"),this.previewElement=this.element.querySelector(".upload-preview"),this.loadingElement=this.element.querySelector(".upload-loading"),this.resultElement=this.element.querySelector(".upload-result"),this.previewImage=this.element.querySelector(".preview-image"),this.fileName=this.element.querySelector(".file-name"),this.fileDetails=this.element.querySelector(".file-details"),this.progressBar=this.element.querySelector(".progress-bar"),this.uploadStatus=this.element.querySelector(".upload-status"),this.setupEventListeners()}setupEventListeners(){this.dropZone.addEventListener("dragenter",this._preventDefaults),this.dropZone.addEventListener("dragover",this._handleDragOver),this.dropZone.addEventListener("dragleave",this._handleDragLeave),this.dropZone.addEventListener("drop",this._handleDrop),this.fileInput.addEventListener("change",this._handleFileSelect),["dragenter","dragover","dragleave","drop"].forEach(t=>{document.addEventListener(t,this._preventDefaults)})}preventDefaults(t){t.preventDefault(),t.stopPropagation()}handleDragOver(t){this.preventDefaults(t),this.dropZone.classList.add("border-primary","bg-light"),this.dropZone.style.borderColor="#0d6efd"}handleDragLeave(t){this.preventDefaults(t),this.dropZone.contains(t.relatedTarget)||(this.dropZone.classList.remove("border-primary","bg-light"),this.dropZone.style.borderColor="#dee2e6")}async handleDrop(t){this.preventDefaults(t),this.dropZone.classList.remove("border-primary","bg-light"),this.dropZone.style.borderColor="#dee2e6";const e=Array.from(t.dataTransfer.files);e.length>0&&await this.processFile(e[0])}async handleFileSelect(t){const e=Array.from(t.target.files);e.length>0&&await this.processFile(e[0])}async processFile(t){const e=this.validateFile(t);e.valid?(this.selectedFile=t,await this.showPreview(t),this.autoUpload&&setTimeout(()=>this.uploadFile(),100)):this.showError(e.error)}validateFile(t){return this.acceptedTypes.includes(t.type)?t.size>this.maxFileSize?{valid:!1,error:`File size (${this.formatFileSize(t.size)}) exceeds maximum allowed size (${this.formatFileSize(this.maxFileSize)})`}:{valid:!0}:{valid:!1,error:`File type "${t.type}" is not supported. Please use: ${this.acceptedTypes.map(t=>t.split("/")[1].toUpperCase()).join(", ")}`}}async showPreview(t){this.previewUrl&&URL.revokeObjectURL(this.previewUrl),this.previewUrl=URL.createObjectURL(t),this.previewImage.src=this.previewUrl,this.fileName.textContent=t.name,this.fileDetails.textContent=`${this.formatFileSize(t.size)} • ${t.type.split("/")[1].toUpperCase()}`,this.promptElement.classList.add("d-none"),this.previewElement.classList.remove("d-none"),this.hideResult(),this.emitUploadEvent("preview",{file:t,previewUrl:this.previewUrl})}async uploadFile(){if(this.selectedFile&&!this.isUploading){this.isUploading=!0,this.showLoading();try{let t;if(this.onUpload&&"function"==typeof this.onUpload)t=await this.onUpload(this.selectedFile,this.updateProgress.bind(this));else{if(!this.uploadUrl)throw new Error("No upload method configured. Provide either uploadUrl or onUpload callback.");t=await this.uploadToUrl(this.selectedFile)}this.showSuccess("File uploaded successfully!"),this.emitUploadEvent("upload-success",{file:this.selectedFile,result:t})}catch(t){console.error("Upload failed:",t),this.showError(`Upload failed: ${t.message}`),this.emitUploadEvent("upload-error",{file:this.selectedFile,error:t})}finally{this.isUploading=!1,this.hideLoading()}}}async uploadToUrl(t){return new Promise((e,i)=>{const s=new FormData;s.append("image",t);const a=new XMLHttpRequest;a.upload.addEventListener("progress",t=>{if(t.lengthComputable){const e=Math.round(t.loaded/t.total*100);this.updateProgress(e)}}),a.addEventListener("load",()=>{if(a.status>=200&&a.status<300)try{const t=JSON.parse(a.responseText);e(t)}catch(t){e({success:!0,response:a.responseText})}else i(new Error(`HTTP ${a.status}: ${a.statusText}`))}),a.addEventListener("error",()=>{i(new Error("Network error occurred"))}),a.addEventListener("timeout",()=>{i(new Error("Upload timeout"))}),a.open("POST",this.uploadUrl),a.timeout=3e4,a.send(s)})}updateProgress(t){this.progressBar&&(this.progressBar.style.width=`${t}%`,this.progressBar.setAttribute("aria-valuenow",t)),this.uploadStatus&&(this.uploadStatus.textContent=`Uploading... ${t}%`)}showLoading(){this.previewElement.classList.add("d-none"),this.loadingElement.classList.remove("d-none"),this.updateProgress(0)}hideLoading(){this.loadingElement.classList.add("d-none"),this.autoUpload&&!this.selectedFile||this.previewElement.classList.remove("d-none")}showSuccess(t){this.showResult("success",t)}showError(t){this.showResult("danger",t)}showResult(t,e){const i=this.resultElement.querySelector(".alert"),s="success"===t?"check-circle-fill":"exclamation-triangle-fill";i.className=`alert alert-${t}`,i.innerHTML=`\n <i class="bi bi-${s} me-2"></i>\n ${e}\n `,this.resultElement.classList.remove("d-none"),"success"===t&&setTimeout(()=>this.hideResult(),5e3)}hideResult(){this.resultElement.classList.add("d-none")}clearFile(){this.previewUrl&&(URL.revokeObjectURL(this.previewUrl),this.previewUrl=null),this.selectedFile=null,this.isUploading=!1,this.fileInput.value="",this.previewElement.classList.add("d-none"),this.loadingElement.classList.add("d-none"),this.promptElement.classList.remove("d-none"),this.hideResult(),this.emitUploadEvent("cleared")}formatFileSize(t){if(0===t)return"0 Bytes";const e=Math.floor(Math.log(t)/Math.log(1024));return parseFloat((t/Math.pow(1024,e)).toFixed(2))+" "+["Bytes","KB","MB","GB"][e]}emitUploadEvent(t,e={}){const i=this.getApp()?.events;i&&i.emit(`imageupload:${t}`,{view:this,...e})}async handleActionSelectFile(){this.fileInput.click()}async handleActionUpload(){await this.uploadFile()}async handleActionClear(){this.clearFile()}async onBeforeDestroy(){this.previewUrl&&(URL.revokeObjectURL(this.previewUrl),this.previewUrl=null),this.dropZone&&(this.dropZone.removeEventListener("dragenter",this._preventDefaults),this.dropZone.removeEventListener("dragover",this._handleDragOver),this.dropZone.removeEventListener("dragleave",this._handleDragLeave),this.dropZone.removeEventListener("drop",this._handleDrop)),this.fileInput&&this.fileInput.removeEventListener("change",this._handleFileSelect),["dragenter","dragover","dragleave","drop"].forEach(t=>{document.removeEventListener(t,this._preventDefaults)}),this.emitUploadEvent("destroyed")}}window.ImageUploadView=ImageUploadView;class LightboxGallery extends t.View{constructor(t={}){super({...t,className:`lightbox-gallery ${t.className||""}`,tagName:"div"});const e=Array.isArray(t.images)?t.images:[t.images||t.src].filter(Boolean);this.images=e.map(t=>"string"==typeof t?{src:t,alt:""}:{src:t.src,alt:t.alt||""}),this.currentIndex=t.startIndex||0,this.showNavigation=!1!==t.showNavigation&&this.images.length>1,this.showCounter=!1!==t.showCounter&&this.images.length>1,this.allowKeyboard=!1!==t.allowKeyboard,this.closeOnBackdrop=!1!==t.closeOnBackdrop,this.fitToScreen=!1!==t.fitToScreen,this._keyboardHandler=this.handleKeyboard.bind(this),this.updateTemplateProperties()}updateTemplateProperties(){this.currentImage=this.images[this.currentIndex]||{src:"",alt:""},this.currentNumber=this.currentIndex+1,this.total=this.images.length,this.isFirst=0===this.currentIndex,this.isLast=this.currentIndex===this.images.length-1,this.imageStyle=this.fitToScreen?"width: 90vw; max-height: 100%; object-fit: contain; user-select: none; cursor: zoom-in;":"max-width: none; max-height: none; user-select: none; cursor: zoom-out;",this.containerStyle=this.fitToScreen?"":"overflow: auto;",this.modeIndicator=this.fitToScreen?"Fit to Screen":"Original Size"}async getTemplate(){return this.images[this.currentIndex],this.images.length,'\n <div class="lightbox-overlay position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"\n style="background: rgba(0,0,0,0.9); z-index: 9999;"\n data-action="backdrop-click">\n\n \x3c!-- Close button --\x3e\n <button type="button" class="btn-close btn-close-white position-absolute top-0 end-0 m-4"\n data-action="close"\n style="z-index: 10001;"\n title="Close"></button>\n\n \x3c!-- Counter --\x3e\n {{#showCounter}}\n <div class="lightbox-counter position-absolute top-0 start-50 translate-middle-x mt-4 text-white"\n style="z-index: 10001; font-size: 1.1rem;">\n {{currentNumber}} of {{total}}\n </div>\n {{/showCounter}}\n\n \x3c!-- Mode Indicator --\x3e\n <div class="lightbox-mode-indicator position-absolute bottom-0 start-50 translate-middle-x mb-4 text-white bg-dark bg-opacity-75 px-3 py-2 rounded"\n style="z-index: 10001; font-size: 0.9rem;">\n {{modeIndicator}} • Click image to toggle\n </div>\n\n \x3c!-- Navigation --\x3e\n {{#showNavigation}}\n <button type="button" class="btn btn-light btn-lg position-absolute start-0 top-50 translate-middle-y ms-4"\n data-action="prev"\n style="z-index: 10001;"\n title="Previous"\n {{#isFirst}}disabled{{/isFirst}}>\n <i class="bi bi-chevron-left"></i>\n </button>\n\n <button type="button" class="btn btn-light btn-lg position-absolute end-0 top-50 translate-middle-y me-4"\n data-action="next"\n style="z-index: 10001;"\n title="Next"\n {{#isLast}}disabled{{/isLast}}>\n <i class="bi bi-chevron-right"></i>\n </button>\n {{/showNavigation}}\n\n \x3c!-- Image container --\x3e\n <div class="lightbox-image-container w-100 h-100 d-flex align-items-center justify-content-center p-5"\n style="{{containerStyle}}">\n {{#currentImage}}\n <img src="{{src}}"\n alt="{{alt}}"\n class="lightbox-image img-fluid"\n style="{{imageStyle}}"\n data-action="image-click">\n {{/currentImage}}\n </div>\n\n \x3c!-- Loading spinner --\x3e\n <div class="lightbox-loading position-absolute top-50 start-50 translate-middle text-white"\n style="display: none;">\n <div class="spinner-border" role="status">\n <span class="visually-hidden">Loading...</span>\n </div>\n </div>\n </div>\n '}async onAfterRender(){document.body.appendChild(this.element),document.body.style.overflow="hidden",this.allowKeyboard&&document.addEventListener("keydown",this._keyboardHandler),this.preloadAdjacentImages()}async handleActionClose(){this.close()}async handleActionBackdropClick(t){this.closeOnBackdrop&&t.target===t.currentTarget&&this.close()}async handleActionPrev(){this.showPrevious()}async handleActionNext(){this.showNext()}async handleActionImageClick(){this.toggleImageMode()}showPrevious(){this.currentIndex>0&&(this.currentIndex--,this.updateImage(),this.preloadAdjacentImages())}showNext(){this.currentIndex<this.images.length-1&&(this.currentIndex++,this.updateImage(),this.preloadAdjacentImages())}goToImage(t){t>=0&&t<this.images.length&&t!==this.currentIndex&&(this.currentIndex=t,this.updateImage(),this.preloadAdjacentImages())}async updateImage(){const t=this.images[this.currentIndex],e=this.element.querySelector(".lightbox-image"),i=this.element.querySelector(".lightbox-counter"),s=this.element.querySelector('[data-action="prev"]'),a=this.element.querySelector('[data-action="next"]');e&&(this.showLoading(),e.src=t.src,e.alt=t.alt,await this.waitForImageLoad(e),this.hideLoading()),i&&(i.textContent=`${this.currentIndex+1} of ${this.images.length}`),s&&(s.disabled=0===this.currentIndex),a&&(a.disabled=this.currentIndex===this.images.length-1),this.updateTemplateProperties(),this.updateImageDisplay();const n=this.getApp()?.events;n&&n.emit("lightbox:image-changed",{gallery:this,index:this.currentIndex,image:t})}showLoading(){const t=this.element.querySelector(".lightbox-loading");t&&(t.style.display="block")}hideLoading(){const t=this.element.querySelector(".lightbox-loading");t&&(t.style.display="none")}waitForImageLoad(t){return new Promise(e=>{t.complete?e():(t.onload=e,t.onerror=e)})}preloadAdjacentImages(){const t=[];this.currentIndex>0&&t.push(this.currentIndex-1),this.currentIndex<this.images.length-1&&t.push(this.currentIndex+1),t.forEach(t=>{const e=this.images[t];(new Image).src=e.src})}handleKeyboard(t){switch(t.key){case"Escape":t.preventDefault(),this.close();break;case"ArrowLeft":t.preventDefault(),this.showPrevious();break;case"ArrowRight":t.preventDefault(),this.showNext();break;case"Home":t.preventDefault(),this.goToImage(0);break;case"End":t.preventDefault(),this.goToImage(this.images.length-1)}}toggleImageMode(){this.fitToScreen=!this.fitToScreen,this.updateTemplateProperties(),this.updateImageDisplay();const t=this.getApp()?.events;t&&t.emit("lightbox:mode-changed",{gallery:this,fitToScreen:this.fitToScreen})}updateImageDisplay(){const t=this.element.querySelector(".lightbox-image"),e=this.element.querySelector(".lightbox-image-container"),i=this.element.querySelector(".lightbox-mode-indicator");t&&(this.fitToScreen?(t.style.maxWidth="100%",t.style.maxHeight="100%",t.style.objectFit="contain",t.style.cursor="zoom-in"):(t.style.maxWidth="none",t.style.maxHeight="none",t.style.objectFit="none",t.style.cursor="zoom-out"),t.style.userSelect="none"),e&&(e.style.overflow=this.fitToScreen?"":"auto"),i&&(i.textContent=`${this.modeIndicator} • Click image to toggle`)}close(){const t=this.getApp()?.events;t&&t.emit("lightbox:closed",{gallery:this}),this.destroy()}async onBeforeDestroy(){document.body.style.overflow="",this.allowKeyboard&&document.removeEventListener("keydown",this._keyboardHandler),this.element.parentNode===document.body&&document.body.removeChild(this.element)}static show(t,e={}){const i=new LightboxGallery({images:t,...e});return i.render().then(()=>{i.mount()}),i}}window.LightboxGallery=LightboxGallery;class PDFViewer extends t.View{constructor(t={}){super({...t,className:`pdf-viewer ${t.className||""}`,tagName:"div"}),this.pdfUrl=t.pdfUrl||t.src||"",this.title=t.title||"PDF Document",this.pdfDoc=null,this.currentPage=1,this.totalPages=0,this.pageRendering=!1,this.pageNumPending=null,this.scale=1,this.minScale=.25,this.maxScale=5,this.scaleStep=.25,this.fitMode="page",this.canvas=null,this.ctx=null,this.showControls=!1!==t.showControls,this.allowZoom=!1!==t.allowZoom,this.allowNavigation=!1!==t.allowNavigation,this.showPageNumbers=!1!==t.showPageNumbers,this.isLoaded=!1,this.isLoading=!1,this.pdfjsWorkerPath=t.pdfjsWorkerPath||"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.worker.min.js",this.pdfjsCMapUrl=t.pdfjsCMapUrl||"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/cmaps/",this.canvasContainer=null,this.controlsElement=null,this.statusElement=null,this.pageInput=null}async getTemplate(){return'\n <div class="pdf-viewer-container">\n {{#showControls}}\n <div class="pdf-viewer-toolbar" data-container="toolbar">\n <div class="btn-toolbar" role="toolbar">\n \x3c!-- Navigation Controls --\x3e\n {{#allowNavigation}}\n <div class="btn-group me-2" role="group" aria-label="Navigation">\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="first-page" title="First Page">\n <i class="bi bi-chevron-double-left"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="prev-page" title="Previous Page">\n <i class="bi bi-chevron-left"></i>\n </button>\n \n {{#showPageNumbers}}\n <div class="input-group input-group-sm" style="width: 120px;">\n <input type="number" class="form-control text-center page-input" min="1" value="1" data-change-action="page-input">\n <span class="input-group-text page-total">/ 0</span>\n </div>\n {{/showPageNumbers}}\n \n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="next-page" title="Next Page">\n <i class="bi bi-chevron-right"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="last-page" title="Last Page">\n <i class="bi bi-chevron-double-right"></i>\n </button>\n </div>\n {{/allowNavigation}}\n\n \x3c!-- Zoom Controls --\x3e\n {{#allowZoom}}\n <div class="btn-group me-2" role="group" aria-label="Zoom">\n <button type="button" class="btn btn-outline-secondary 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="zoom-in" title="Zoom In">\n <i class="bi bi-zoom-in"></i>\n </button>\n \n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" title="Fit">\n <i class="bi bi-arrows-fullscreen"></i>\n </button>\n <ul class="dropdown-menu">\n <li><a class="dropdown-item" href="#" data-action="fit-page">Fit Page</a></li>\n <li><a class="dropdown-item" href="#" data-action="fit-width">Fit Width</a></li>\n <li><a class="dropdown-item" href="#" data-action="actual-size">Actual Size</a></li>\n </ul>\n </div>\n </div>\n {{/allowZoom}}\n\n \x3c!-- Utility Controls --\x3e\n <div class="btn-group me-2" role="group" aria-label="Utilities">\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="download" title="Download">\n <i class="bi bi-download"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="print" title="Print">\n <i class="bi bi-printer"></i>\n </button>\n </div>\n </div>\n </div>\n {{/showControls}}\n\n \x3c!-- PDF Content Area --\x3e\n <div class="pdf-viewer-content" data-container="content">\n <div class="pdf-canvas-container" data-container="canvasContainer">\n <canvas class="pdf-canvas" data-container="canvas"></canvas>\n </div>\n\n <div class="pdf-viewer-overlay">\n <div class="pdf-viewer-loading">\n <div class="spinner-border text-primary" role="status">\n <span class="visually-hidden">Loading PDF...</span>\n </div>\n <div class="mt-2">Loading PDF...</div>\n </div>\n </div>\n\n <div class="pdf-viewer-error" style="display: none;">\n <div class="alert alert-danger" role="alert">\n <i class="bi bi-exclamation-triangle"></i>\n <strong>Error:</strong> <span class="error-message">Failed to load PDF</span>\n </div>\n </div>\n </div>\n\n \x3c!-- Status Bar --\x3e\n <div class="pdf-viewer-status" data-container="status">\n <small class="text-muted">\n <span class="current-page">0</span> of <span class="total-pages">0</span> pages |\n <span class="zoom-level">100%</span> |\n <span class="document-title">{{title}}</span>\n </small>\n </div>\n </div>\n '}get(){return{pdfUrl:this.pdfUrl,title:this.title,showControls:this.showControls,allowZoom:this.allowZoom,allowNavigation:this.allowNavigation,showPageNumbers:this.showPageNumbers}}async onAfterRender(){this.canvas=this.element.querySelector(".pdf-canvas"),this.canvasContainer=this.element.querySelector(".pdf-canvas-container"),this.controlsElement=this.element.querySelector(".pdf-viewer-toolbar"),this.statusElement=this.element.querySelector(".pdf-viewer-status"),this.pageInput=this.element.querySelector(".page-input"),this.overlayElement=this.element.querySelector(".pdf-viewer-overlay"),this.errorElement=this.element.querySelector(".pdf-viewer-error"),this.canvas&&(this.ctx=this.canvas.getContext("2d")),this.setupEssentialEventListeners(),await this.initializePDFJS(),this.pdfUrl&&await this.loadPDF()}setupEssentialEventListeners(){const t=t=>this.handleKeyDown(t);document.addEventListener("keydown",t);let e=null;this.canvasContainer&&(e=new ResizeObserver(()=>{"auto"!==this.fitMode&&this.applyFitMode()}),e.observe(this.canvasContainer)),this._essentialListeners=[{el:document,type:"keydown",fn:t}],this._resizeObserver=e}async initializePDFJS(){try{return void 0===window.pdfjsLib&&await this.loadPDFJSLibrary(),window.pdfjsLib.GlobalWorkerOptions.workerSrc=this.pdfjsWorkerPath,!0}catch(t){return console.error("Failed to initialize PDF.js:",t),this.showError("Failed to initialize PDF viewer"),!1}}async loadPDFJSLibrary(){return new Promise((t,e)=>{const i=document.createElement("script");i.src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.min.js",i.onload=t,i.onerror=e,document.head.appendChild(i)})}async handleActionFirstPage(){await this.goToPage(1)}async handleActionPrevPage(){await this.goToPage(this.currentPage-1)}async handleActionNextPage(){await this.goToPage(this.currentPage+1)}async handleActionLastPage(){await this.goToPage(this.totalPages)}async onChangePageInput(t,e){const i=parseInt(e.value,10);i>=1&&i<=this.totalPages?await this.goToPage(i):e.value=this.currentPage}async handleActionZoomIn(){this.setScale(this.scale+this.scaleStep)}async handleActionZoomOut(){this.setScale(this.scale-this.scaleStep)}async handleActionFitPage(){this.setFitMode("page")}async handleActionFitWidth(){this.setFitMode("width")}async handleActionActualSize(){this.setScale(1),this.fitMode="auto"}async handleActionDownload(){this.downloadPDF()}async handleActionPrint(){window.print()}async loadPDF(){if(!this.pdfUrl||!window.pdfjsLib)return this.showError("PDF URL or PDF.js library not available"),!1;this.isLoading=!0,this.showLoading();try{const t=window.pdfjsLib.getDocument({url:this.pdfUrl,cMapUrl:this.pdfjsCMapUrl,cMapPacked:!0});this.pdfDoc=await t.promise,this.totalPages=this.pdfDoc.numPages,this.currentPage=1,this.updatePageControls(),this.updateStatus(),await this.renderPage(1),this.isLoaded=!0,this.isLoading=!1,this.element.classList.add("loaded"),this.hideLoading(),this.applyFitMode();const e=this.getApp()?.events;return e&&e.emit("pdfviewer:loaded",{viewer:this,pdfUrl:this.pdfUrl,totalPages:this.totalPages}),!0}catch(t){console.error("Error loading PDF:",t),this.isLoading=!1,this.showError("Failed to load PDF document");const e=this.getApp()?.events;return e&&e.emit("pdfviewer:error",{viewer:this,pdfUrl:this.pdfUrl,error:t.message}),!1}}async renderPage(t){if(this.pageRendering)this.pageNumPending=t;else if(this.pdfDoc&&this.canvas&&this.ctx){this.pageRendering=!0,this.currentPage=t;try{const e=await this.pdfDoc.getPage(t),i=e.getViewport({scale:this.scale});this.canvas.height=i.height,this.canvas.width=i.width,this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);const s={canvasContext:this.ctx,viewport:i},a=e.render(s);if(await a.promise,this.pageRendering=!1,null!==this.pageNumPending){const t=this.pageNumPending;this.pageNumPending=null,await this.renderPage(t)}this.updatePageControls(),this.updateStatus();const n=this.getApp()?.events;n&&n.emit("pdfviewer:page-changed",{viewer:this,currentPage:this.currentPage,totalPages:this.totalPages})}catch(e){console.error("Error rendering page:",e),this.pageRendering=!1,this.showError("Failed to render PDF page")}}}async goToPage(t){!this.pdfDoc||t<1||t>this.totalPages||await this.renderPage(t)}setScale(t){const e=this.scale;this.scale=Math.max(this.minScale,Math.min(this.maxScale,t)),this.fitMode="auto",this.isLoaded&&this.renderPage(this.currentPage);const i=this.getApp()?.events;i&&e!==this.scale&&i.emit("pdfviewer:scale-changed",{viewer:this,oldScale:e,newScale:this.scale})}setFitMode(t){const e=this.fitMode;this.fitMode=t,this.applyFitMode();const i=this.getApp()?.events;i&&i.emit("pdfviewer:fit-mode-changed",{viewer:this,oldMode:e,newMode:t})}applyFitMode(){this.isLoaded&&this.pdfDoc&&this.canvasContainer&&this.pdfDoc.getPage(this.currentPage).then(t=>{const e=this.canvasContainer.getBoundingClientRect(),i=t.getViewport({scale:1});let s;if("page"===this.fitMode){const t=(e.width-40)/i.width,a=(e.height-40)/i.height;s=Math.min(t,a)}else{if("width"!==this.fitMode)return;s=(e.width-40)/i.width}this.scale=Math.max(this.minScale,Math.min(this.maxScale,s)),this.renderPage(this.currentPage)})}handleKeyDown(t){if("INPUT"!==t.target.tagName||t.target===this.pageInput)switch(t.key){case"ArrowLeft":case"PageUp":t.preventDefault(),this.goToPage(this.currentPage-1);break;case"ArrowRight":case"PageDown":t.preventDefault(),this.goToPage(this.currentPage+1);break;case"Home":t.preventDefault(),this.goToPage(1);break;case"End":t.preventDefault(),this.goToPage(this.totalPages);break;case"+":case"=":(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.setScale(this.scale+this.scaleStep));break;case"-":(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.setScale(this.scale-this.scaleStep));break;case"0":(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.setFitMode("page"))}}updatePageControls(){this.pageInput&&(this.pageInput.value=this.currentPage);const t=this.element.querySelector(".page-total");t&&(t.textContent=`/ ${this.totalPages}`);const e=this.element.querySelector('[data-action="first-page"]'),i=this.element.querySelector('[data-action="prev-page"]'),s=this.element.querySelector('[data-action="next-page"]'),a=this.element.querySelector('[data-action="last-page"]');e&&(e.disabled=this.currentPage<=1),i&&(i.disabled=this.currentPage<=1),s&&(s.disabled=this.currentPage>=this.totalPages),a&&(a.disabled=this.currentPage>=this.totalPages);const n=this.element.querySelector('[data-action="zoom-in"]'),o=this.element.querySelector('[data-action="zoom-out"]');n&&(n.disabled=this.scale>=this.maxScale),o&&(o.disabled=this.scale<=this.minScale)}updateStatus(){if(!this.statusElement)return;const t=this.statusElement.querySelector(".current-page"),e=this.statusElement.querySelector(".total-pages"),i=this.statusElement.querySelector(".zoom-level");t&&(t.textContent=this.currentPage),e&&(e.textContent=this.totalPages),i&&(i.textContent=`${Math.round(100*this.scale)}%`)}showLoading(){this.overlayElement&&(this.overlayElement.style.display="flex")}hideLoading(){this.overlayElement&&(this.overlayElement.style.display="none")}showError(t){if(this.hideLoading(),this.errorElement){const e=this.errorElement.querySelector(".error-message");e&&(e.textContent=t),this.errorElement.style.display="block"}console.error("PDF Viewer Error:",t)}downloadPDF(){if(!this.pdfUrl)return;const t=document.createElement("a");t.href=this.pdfUrl,t.download=this.title+".pdf",t.target="_blank",t.click()}setPDF(t,e=""){const i=this.pdfUrl;this.pdfUrl=t,this.title=e||"PDF Document",this.isLoaded=!1,this.element.classList.remove("loaded"),this.pdfDoc&&(this.pdfDoc.destroy(),this.pdfDoc=null),this.currentPage=1,this.totalPages=0,this.scale=1,t&&this.loadPDF();const s=this.getApp()?.events;s&&s.emit("pdfviewer:pdf-changed",{viewer:this,oldPdfUrl:i,newPdfUrl:t})}getCurrentPage(){return this.currentPage}getTotalPages(){return this.totalPages}getCurrentScale(){return this.scale}async onBeforeDestroy(){this.pdfDoc&&(this.pdfDoc.destroy(),this.pdfDoc=null),this.pageRendering=!1,this.pageNumPending=null,this._essentialListeners&&(this._essentialListeners.forEach(({el:t,type:e,fn:i})=>{t&&t.removeEventListener(e,i)}),this._essentialListeners=null),this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null);const t=this.getApp()?.events;t&&t.emit("pdfviewer:destroyed",{viewer:this})}static async showDialog(t,i={}){const{title:s="PDF Viewer",size:a="fullscreen",showControls:n=!0,allowZoom:o=!0,allowNavigation:r=!0,showPageNumbers:l=!0,...h}=i,c=new PDFViewer({pdfUrl:t,title:s,showControls:n,allowZoom:o,allowNavigation:r,showPageNumbers:l}),d=new e.ModalView({title:s,body:c,size:a,centered:!0,backdrop:"static",keyboard:!0,buttons:[{text:"Download",action:"download",class:"btn btn-outline-primary"},{text:"Close",action:"close",class:"btn btn-secondary",dismiss:!0}],...h});return await d.render(),document.body.appendChild(d.element),await d.mount(),d.show(),new Promise(t=>{d.on("hidden",()=>{d.destroy(),t(c)}),d.on("action:download",()=>{c.downloadPDF()}),d.on("action:close",()=>{d.hide()})})}}exports.WebApp=i.WebApp,exports.BUILD_TIME=s.BUILD_TIME,exports.VERSION=s.VERSION,exports.VERSION_INFO=s.VERSION_INFO,exports.VERSION_MAJOR=s.VERSION_MAJOR,exports.VERSION_MINOR=s.VERSION_MINOR,exports.VERSION_REVISION=s.VERSION_REVISION,exports.ImageCanvasView=ImageCanvasView,exports.ImageCropView=ImageCropView,exports.ImageEditor=ImageEditor,exports.ImageFiltersView=ImageFiltersView,exports.ImageTransformView=ImageTransformView,exports.ImageUploadView=ImageUploadView,exports.ImageViewer=ImageViewer,exports.LightboxGallery=LightboxGallery,exports.PDFViewer=PDFViewer;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./chunks/View-CPWwS19u.js"),e=require("./chunks/Modal-Bm1OQ8Ou.js"),i=require("./chunks/WebApp-BdxhRnnT.js"),s=require("./chunks/version-BYy2UAT0.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" data-container="controls">\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(){if(this.canvas&&this.containerElement){if(this.context&&(this.context.imageSmoothingEnabled=!0,this.context.imageSmoothingQuality="high"),this.resizeCanvas(),!this._sizingPoll&&(!this.canvasWidth||this.canvasWidth<=1||!this.canvasHeight||this.canvasHeight<=1)){const t=Date.now();this._sizingPoll=setInterval(()=>{if(!this.canvas||!this.containerElement)return clearInterval(this._sizingPoll),void(this._sizingPoll=null);this.resizeCanvas(),(this.canvasWidth>1&&this.canvasHeight>1||Date.now()-t>2e3)&&(clearInterval(this._sizingPoll),this._sizingPoll=null)},50)}"undefined"==typeof ResizeObserver||this._resizeObserver||(this._resizeObserver=new ResizeObserver(()=>this.resizeCanvas()),this._resizeObserver.observe(this.containerElement))}}resizeCanvas(){if(!this.canvas||!this.containerElement)return;const t=Math.max(1,Math.floor(this.containerElement.clientWidth)),e=Math.max(1,Math.floor(this.containerElement.clientHeight));if(t===this.canvasWidth&&e===this.canvasHeight)return;const i=window.devicePixelRatio||1;this.canvasWidth=t,this.canvasHeight=e,this.canvas.width=t*i,this.canvas.height=e*i,this.canvas.style.width=t+"px",this.canvas.style.height=e+"px",this.context.setTransform(i,0,0,i,0,0),this.isLoaded&&this.image&&this.autoFit?this.fitToContainer():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"),requestAnimationFrame(()=>{this.canvasWidth&&this.canvasHeight||this.resizeCanvas(),this.canvasWidth&&this.canvasHeight&&(this.autoFit?this.fitToContainer():this.smartFit(),this.renderCanvas(),this.updateControls())});const t=this.getApp()?.events;t&&t.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(),i=t.clientX-e.left,s=t.clientY-e.top,a=i-this.lastPointerX,n=s-this.lastPointerY;this.pan(a,n),this.lastPointerX=i,this.lastPointerY=s}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(),i=t.clientX-e.left,s=t.clientY-e.top,a=t.deltaY>0?.5*-this.scaleStep:.5*this.scaleStep;this.zoomAtPoint(this.scale+a,i,s)}handleTouchStart(t){if(1===t.touches.length&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect();this.isDragging=!0,this.lastPointerX=e.clientX-i.left,this.lastPointerY=e.clientY-i.top}}handleTouchMove(t){if(1===t.touches.length&&this.isDragging&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect(),s=e.clientX-i.left,a=e.clientY-i.top,n=s-this.lastPointerX,o=a-this.lastPointerY;this.pan(n,o),this.lastPointerX=s,this.lastPointerY=a}}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 i=this.getApp()?.events;i&&e!==this.scale&&i.emit("imageviewer:scale-changed",{viewer:this,oldScale:e,newScale:this.scale})}zoomAtPoint(t,e,i){if(!this.image)return;const s=this.scale;if(this.setScale(t),s!==this.scale){const t=this.scale/s,a=this.canvasWidth/2,n=this.canvasHeight/2;this.translateX=(this.translateX-(e-a))*t+(e-a),this.translateY=(this.translateY-(i-n))*t+(i-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 i=this.getApp()?.events;i&&i.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,i=t/this.image.naturalWidth,s=e/this.image.naturalHeight,a=Math.min(i,s,1);this.setScale(a),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,i=Math.min(t,e);i<1&&this.setScale(i),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"]'),i=this.controlsElement.querySelector('[data-action="zoom-out"]');e&&(e.disabled=this.scale>=this.maxScale),i&&(i.disabled=this.scale<=this.minScale)}setImage(t,e="",i=""){const s=this.imageUrl;this.imageUrl=t,this.alt=e,this.title=i,this.reset(),this.loadImage(t);const a=this.getApp()?.events;a&&a.emit("imageviewer:image-changed",{viewer:this,oldImageUrl:s,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),this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),this._sizingPoll&&(clearInterval(this._sizingPoll),this._sizingPoll=null);const t=this.getApp()?.events;t&&t.emit("imageviewer:destroyed",{viewer:this})}static async showDialog(t,i={}){const{title:s="Image Viewer",alt:a="Image",size:n="fullscreen",showControls:o=!0,allowRotate:r=!0,allowZoom:l=!0,allowPan:h=!0,allowDownload:c=!0,...d}=i,m=new ImageViewer({imageUrl:t,alt:a,title:s,showControls:o,allowRotate:r,allowZoom:l,allowPan:h,allowDownload:c,autoFit:!0});return e.Modal.dialog({title:s,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 i,s;if("fullscreen"===t)i=Math.min(1200,.9*window.innerWidth),s=Math.min(900,.8*window.innerHeight);else if("auto"!==t&&e){const t=window.innerWidth*this.maxCanvasWidthPercent,a=window.innerHeight*this.maxCanvasHeightPercent;if(e.width>t||e.height>a)if(this.image){const e=t/this.image.naturalWidth,n=a/this.image.naturalHeight,o=Math.min(e,n,1);i=Math.floor(this.image.naturalWidth*o),s=Math.floor(this.image.naturalHeight*o),i=Math.max(300,i),s=Math.max(200,s)}else i=Math.min(600,t),s=Math.min(450,a);else i=e.width,s=e.height}else if(this.image){const t=window.innerWidth*this.maxCanvasWidthPercent,e=window.innerHeight*this.maxCanvasHeightPercent,a=t/this.image.naturalWidth,n=e/this.image.naturalHeight,o=Math.min(a,n,1);i=Math.floor(this.image.naturalWidth*o),s=Math.floor(this.image.naturalHeight*o),i=Math.max(300,i),s=Math.max(200,s)}else i=Math.min(600,window.innerWidth*this.maxCanvasWidthPercent),s=Math.min(450,window.innerHeight*this.maxCanvasHeightPercent);if(i=Math.min(i,window.innerWidth*this.maxCanvasWidthPercent),s=Math.min(s,window.innerHeight*this.maxCanvasHeightPercent),Math.abs(i-this.canvasWidth)<10&&Math.abs(s-this.canvasHeight)<10)return;const a=window.devicePixelRatio||1;this.canvasWidth=i,this.canvasHeight=s,this.canvas.width=i*a,this.canvas.height=s*a,this.canvas.style.width=i+"px",this.canvas.style.height=s+"px",this.context.setTransform(a,0,0,a,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,i=Math.min(t,e,1),s=this.image.naturalWidth*i,a=this.image.naturalHeight*i,n=(this.canvasWidth-s)/2,o=(this.canvasHeight-a)/2;this.context.drawImage(this.image,n,o,s,a)}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(i){console.error("Failed to export image blob:",i),e(null)}}):Promise.resolve(null)}setImage(t,e="",i=""){const s=this.imageUrl;this.alt=e,this.title=i,this.loadImage(t);const a=this.getApp()?.events;a&&a.emit("imagecanvas:image-changed",{view:this,oldImageUrl:s,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(),i=t.clientX-e.left,s=t.clientY-e.top,a=i-this.lastPointerX,n=s-this.lastPointerY;this.pan(a,n),this.lastPointerX=i,this.lastPointerY=s}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(),i=t.clientX-e.left,s=t.clientY-e.top,a=t.deltaY>0?.5*-this.scaleStep:.5*this.scaleStep;this.zoomAtPoint(this.scale+a,i,s)}handleTouchStart(t){if(1===t.touches.length&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect();this.isDragging=!0,this.lastPointerX=e.clientX-i.left,this.lastPointerY=e.clientY-i.top}}handleTouchMove(t){if(1===t.touches.length&&this.isDragging&&this.allowPan){t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect(),s=e.clientX-i.left,a=e.clientY-i.top,n=s-this.lastPointerX,o=a-this.lastPointerY;this.pan(n,o),this.lastPointerX=s,this.lastPointerY=a}}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,i){if(!this.image)return;const s=this.scale;if(this.setScale(t),s!==this.scale){const t=this.scale/s,a=this.canvasWidth/2,n=this.canvasHeight/2;this.translateX=(this.translateX-(e-a))*t+(e-a),this.translateY=(this.translateY-(i-n))*t+(i-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,i=t/this.image.naturalWidth,s=e/this.image.naturalHeight,a=Math.min(i,s,1);this.setScale(a),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,i=Math.min(t,e);i<1&&this.setScale(i),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 i=this.getApp()?.events;i&&i.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,i={}){const{title:s="Transform Image",alt:a="Image",size:n="xl",allowPan:o=!0,allowZoom:r=!0,allowRotate:l=!0,...h}=i,c=new ImageTransformView({imageUrl:t,alt:a,title:s,allowPan:o,allowZoom:r,allowRotate:l}),d=new e.ModalView({title:s,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,i=this.canvasHeight/this.image.naturalHeight;let s;s=this.autoFit?Math.min(e,i,1):1;const a=this.image.naturalWidth*s,n=this.image.naturalHeight*s,o=(this.canvasWidth-a)/2,r=(this.canvasHeight-n)/2;return{x:t.x*s+o,y:t.y*s+r,width:t.width*s,height:t.height*s}}canvasToImage(t){if(!this.image)return t;const e=this.canvasWidth/this.image.naturalWidth,i=this.canvasHeight/this.image.naturalHeight;let s;s=this.autoFit?Math.min(e,i,1):1;const a=this.image.naturalWidth*s,n=this.image.naturalHeight*s,o=(this.canvasWidth-a)/2,r=(this.canvasHeight-n)/2;return{x:(t.x-o)/s,y:(t.y-r)/s,width:t.width/s,height:t.height/s}}pointCanvasToImage(t,e){const i=this.canvasToImage({x:t,y:e,width:0,height:0});return{x:i.x,y:i.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 i;i=this.autoFit?Math.min(t,e,1):1;const s=this.image.naturalWidth*i,a=this.image.naturalHeight*i;this.imageOffsetX=(this.canvasWidth-s)/2,this.imageOffsetY=(this.canvasHeight-a)/2,this.imageScale=i,this.imageOffsetX,this.imageOffsetY,this.imageScale,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 i;i=this.autoFit?Math.min(t,e,1):1;const s=this.image.naturalWidth*i,a=this.image.naturalHeight*i,n=(this.canvasWidth-s)/2,o=(this.canvasHeight-a)/2;this.context.drawImage(this.image,n,o,s,a)}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{this.cropBox;const i={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)};let s=i.width,a=i.height;this.cropAndScale&&(s=this.cropAndScale.width,a=this.cropAndScale.height);const n=document.createElement("canvas");n.width=s,n.height=a,n.getContext("2d").drawImage(this.image,i.x,i.y,i.width,i.height,0,0,s,a),n.toBlob(t=>{e(t)},"image/png",t)}catch(i){console.error("Failed to export cropped image blob:",i),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,i=t.height/3;for(let s=1;s<3;s++){const i=t.x+e*s;this.context.beginPath(),this.context.moveTo(i,t.y),this.context.lineTo(i,t.y+t.height),this.context.stroke()}for(let s=1;s<3;s++){const e=t.y+i*s;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 i=this.handles[e],s=t.x+t.width*i.x,a=t.y+t.height*i.y,n=s-this.handleSize/2,o=a-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(),i=t.clientX-e.left,s=t.clientY-e.top,a=this.pointCanvasToImage(i,s);if(this.dragStartImageX=a.x,this.dragStartImageY=a.y,this.initialCropBox={...this.cropBox},this.fixedCropSize)this.isPointInCropBox(i,s)&&(this.isDragging=!0,this.canvas.style.cursor="move");else{const t=this.getHandleAt(i,s);t?(this.isResizing=!0,this.dragHandle=t,this.canvas.style.cursor=this.handles[t].cursor):this.isPointInCropBox(i,s)?(this.isDragging=!0,this.canvas.style.cursor="move"):this.startNewCrop(a.x,a.y)}}handleMouseMove(t){if(!this.cropMode)return;const e=this.canvas.getBoundingClientRect(),i=t.clientX-e.left,s=t.clientY-e.top;this.isResizing&&this.dragHandle?this.resizeCropBox(i,s):this.isDragging?this.moveCropBox(i,s):this.isDragging||this.isResizing||this.updateCursor(i,s)}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(),i=t.clientX-e.left,s=t.clientY-e.top;this.updateCursor(i,s)}handleTouchStart(t){if(!this.cropMode||1!==t.touches.length)return;t.preventDefault();const e=t.touches[0],i=this.canvas.getBoundingClientRect();e.clientX,i.left,e.clientY,i.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 i=this.imageToCanvas(this.cropBox);for(const[s,a]of Object.entries(this.handles)){const n=i.x+i.width*a.x,o=i.y+i.height*a.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 s}return null}isPointInCropBox(t,e){const i=this.pointCanvasToImage(t,e);return i.x>=this.cropBox.x&&i.x<=this.cropBox.x+this.cropBox.width&&i.y>=this.cropBox.y&&i.y<=this.cropBox.y+this.cropBox.height}updateCursor(t,e){if(!this.cropMode)return;const i=t,s=e;if(this.fixedCropSize)this.isPointInCropBox(i,s)?this.canvas.style.cursor="move":this.canvas.style.cursor="default";else{const a=this.getHandleAt(t,e);a?this.canvas.style.cursor=this.handles[a].cursor:this.isPointInCropBox(i,s)?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 i=this.pointCanvasToImage(t,e);if(this.newCropStart){const t=this.newCropStart.x,e=this.newCropStart.y;return this.cropBox={x:Math.min(t,i.x),y:Math.min(e,i.y),width:Math.abs(i.x-t),height:Math.abs(i.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 s=i.x-this.dragStartImageX,a=i.y-this.dragStartImageY;let n={...this.initialCropBox};switch(this.dragHandle){case"nw":n.x+=s,n.y+=a,n.width-=s,n.height-=a;break;case"ne":n.y+=a,n.width+=s,n.height-=a;break;case"sw":n.x+=s,n.width-=s,n.height+=a;break;case"se":n.width+=s,n.height+=a;break;case"n":n.y+=a,n.height-=a;break;case"s":n.height+=a;break;case"w":n.x+=s,n.width-=s;break;case"e":n.width+=s}this.aspectRatio&&this.constrainToAspectRatio(n,this.dragHandle),this.constrainCropBox(n),this.cropBox=n,this.renderCanvas()}moveCropBox(t,e){if(!this.initialCropBox)return;const i=this.pointCanvasToImage(t,e),s=i.x-this.dragStartImageX,a=i.y-this.dragStartImageY;let n={x:this.initialCropBox.x+s,y:this.initialCropBox.y+a,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 i,s,a=this.aspectRatio;if(this.cropAndScale&&(a=this.cropAndScale.width/this.cropAndScale.height),a)if(["nw","ne","sw","se"].includes(e)){switch(e){case"nw":i=t.x+t.width,s=t.y+t.height;break;case"ne":i=t.x,s=t.y+t.height;break;case"sw":i=t.x+t.width,s=t.y;break;case"se":i=t.x,s=t.y}switch(t.width/t.height>a?t.width=t.height*a:t.height=t.width/a,e){case"nw":t.x=i-t.width,t.y=s-t.height;break;case"ne":t.x=i,t.y=s-t.height;break;case"sw":t.x=i-t.width,t.y=s;break;case"se":t.x=i,t.y=s}}else if(["n","s"].includes(e)){const e=t.x+t.width/2;t.width=t.height*a,t.x=e-t.width/2}else if(["w","e"].includes(e)){const e=t.y+t.height/2;t.height=t.width/a,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||(this.cropMode=!0,this.initializeCropBox(),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 i,s;if(this.fixedCropSize)i=this.fixedCropSize.width,s=this.fixedCropSize.height;else{i=Math.floor(.8*t),s=Math.floor(.8*e);let a=this.aspectRatio;this.cropAndScale&&(a=this.cropAndScale.width/this.cropAndScale.height),this.aspectRatio=a,a&&(i/s>a?i=s*a:s=i/a),i=Math.max(this.minCropSize||50,i),s=Math.max(this.minCropSize||50,s)}const a=Math.floor((t-i)/2),n=Math.floor((e-s)/2);this.cropBox={x:a,y:n,width:i,height:s}}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"),i=e.getContext("2d");this.cropAndScale?(e.width=this.cropAndScale.width,e.height=this.cropAndScale.height,i.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,i.drawImage(this.image,t.x,t.y,t.width,t.height,0,0,t.width,t.height));const s=e.toDataURL("image/png");return{canvas:e,imageData:s,cropData:t}}emitCropEvent(t,e={}){const i=this.getApp()?.events;i&&i.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 i=e.getAttribute("data-ratio"),s="free"===i?null:parseFloat(i);this.setAspectRatio(s)}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,i={}){const{title:s="Crop Image",alt:a="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}=i,p=new ImageCropView({imageUrl:t,alt:a,title:s,aspectRatio:o,minCropSize:r,canvasSize:g||n||"md",fixedCropSize:d,cropAndScale:m,showGrid:l,showToolbar:h,autoFit:c}),v=new e.ModalView({title:s,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-flex flex-column h-100">\n {{#showControls}}\n \x3c!-- Filter Toolbar --\x3e\n <div class="image-filters-toolbar bg-light border-bottom p-2">\n <div class="btn-toolbar justify-content-center flex-wrap" role="toolbar">\n\n {{#showPresets}}\n \x3c!-- Preset Effects --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Preset effects">\n <div class="dropdown">\n <button type="button" class="btn btn-outline-primary btn-sm dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false" title="Preset Effects">\n <i class="bi bi-palette"></i> Effects\n </button>\n <ul class="dropdown-menu">\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="none">Original</a></li>\n <li><hr class="dropdown-divider"></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="blackWhite">Black & White</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="sepia">Sepia</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="vintage">Vintage</a></li>\n <li><hr class="dropdown-divider"></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="cool">Cool Tones</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="warm">Warm Tones</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="vibrant">Vibrant</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="dramatic">Dramatic</a></li>\n <li><a class="dropdown-item" href="#" data-action="apply-preset" data-preset="soft">Soft</a></li>\n </ul>\n </div>\n </div>\n {{/showPresets}}\n\n {{#showBasicControls}}\n {{#controlsInDropdowns}}\n \x3c!-- Basic Controls in Dropdown --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Basic controls">\n <div class="dropdown">\n <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false" title="Basic Adjustments">\n <i class="bi bi-sliders"></i> Basic\n </button>\n <div class="dropdown-menu p-3" style="min-width: 300px;">\n <div class="mb-3">\n <label class="form-label small fw-bold">Brightness</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.brightness}}"\n data-change-action="filter-change" data-filter="brightness">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="brightness">{{filters.brightness}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label small fw-bold">Contrast</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.contrast}}"\n data-change-action="filter-change" data-filter="contrast">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="contrast">{{filters.contrast}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="mb-0">\n <label class="form-label small fw-bold">Saturation</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.saturation}}"\n data-change-action="filter-change" data-filter="saturation">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="saturation">{{filters.saturation}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/controlsInDropdowns}}\n {{/showBasicControls}}\n\n {{#showAdvancedControls}}\n {{#controlsInDropdowns}}\n \x3c!-- Advanced Controls in Dropdown --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Advanced controls">\n <div class="dropdown">\n <button type="button" class="btn btn-outline-warning btn-sm dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false" title="Advanced Adjustments">\n <i class="bi bi-gear"></i> Advanced\n </button>\n <div class="dropdown-menu p-3" style="min-width: 300px;">\n <div class="mb-3">\n <label class="form-label small fw-bold">Hue</label>\n <input type="range" class="form-range"\n min="0" max="360" value="{{filters.hue}}"\n data-change-action="filter-change" data-filter="hue">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0°</small>\n <small class="text-muted filter-value" data-filter="hue">{{filters.hue}}°</small>\n <small class="text-muted">360°</small>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label small fw-bold">Blur</label>\n <input type="range" class="form-range"\n min="0" max="10" value="{{filters.blur}}"\n data-change-action="filter-change" data-filter="blur">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0px</small>\n <small class="text-muted filter-value" data-filter="blur">{{filters.blur}}px</small>\n <small class="text-muted">10px</small>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label small fw-bold">Grayscale</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.grayscale}}"\n data-change-action="filter-change" data-filter="grayscale">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="grayscale">{{filters.grayscale}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n <div class="mb-0">\n <label class="form-label small fw-bold">Sepia</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.sepia}}"\n data-change-action="filter-change" data-filter="sepia">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="sepia">{{filters.sepia}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/controlsInDropdowns}}\n {{/showAdvancedControls}}\n\n {{#allowReset}}\n \x3c!-- Reset & Preview Controls --\x3e\n <div class="btn-group me-2 mb-2" role="group" aria-label="Reset controls">\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="reset-filters" title="Reset All Filters">\n <i class="bi bi-arrow-repeat"></i> Reset\n </button>\n <button type="button" class="btn btn-outline-info btn-sm" data-action="preview-original" title="Preview Original"\n onmousedown="this.dataset.previewing=\'true\'"\n onmouseup="this.dataset.previewing=\'false\'"\n onmouseleave="this.dataset.previewing=\'false\'">\n <i class="bi bi-eye"></i> Original\n </button>\n </div>\n {{/allowReset}}\n\n </div>\n </div>\n {{/showControls}}\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-filters-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\n {{#showControls}}\n {{^controlsInDropdowns}}\n \x3c!-- Expanded Controls Panel (when not in dropdowns) --\x3e\n <div class="image-filters-controls bg-light border-top p-3" data-container="controls" style="max-height: 300px; overflow-y: auto;">\n <div class="row g-3">\n {{#showBasicControls}}\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Brightness</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.brightness}}"\n data-change-action="filter-change" data-filter="brightness">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="brightness">{{filters.brightness}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Contrast</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.contrast}}"\n data-change-action="filter-change" data-filter="contrast">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="contrast">{{filters.contrast}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Saturation</label>\n <input type="range" class="form-range"\n min="0" max="200" value="{{filters.saturation}}"\n data-change-action="filter-change" data-filter="saturation">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="saturation">{{filters.saturation}}%</small>\n <small class="text-muted">200%</small>\n </div>\n </div>\n {{/showBasicControls}}\n\n {{#showAdvancedControls}}\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Hue</label>\n <input type="range" class="form-range"\n min="0" max="360" value="{{filters.hue}}"\n data-change-action="filter-change" data-filter="hue">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0°</small>\n <small class="text-muted filter-value" data-filter="hue">{{filters.hue}}°</small>\n <small class="text-muted">360°</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Blur</label>\n <input type="range" class="form-range"\n min="0" max="10" value="{{filters.blur}}"\n data-change-action="filter-change" data-filter="blur">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0px</small>\n <small class="text-muted filter-value" data-filter="blur">{{filters.blur}}px</small>\n <small class="text-muted">10px</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Grayscale</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.grayscale}}"\n data-change-action="filter-change" data-filter="grayscale">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="grayscale">{{filters.grayscale}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n <div class="col-md-6 col-lg-4">\n <label class="form-label small fw-bold">Sepia</label>\n <input type="range" class="form-range"\n min="0" max="100" value="{{filters.sepia}}"\n data-change-action="filter-change" data-filter="sepia">\n <div class="d-flex justify-content-between">\n <small class="text-muted">0%</small>\n <small class="text-muted filter-value" data-filter="sepia">{{filters.sepia}}%</small>\n <small class="text-muted">100%</small>\n </div>\n </div>\n {{/showAdvancedControls}}\n </div>\n </div>\n {{/controlsInDropdowns}}\n {{/showControls}}\n </div>\n '}async onAfterRender(){await super.onAfterRender(),this.controlsElement=this.element.querySelector(".image-filters-controls")}setupCanvas(){this.canvas&&this.containerElement&&(this.setCanvasSize(this.canvasSize),"fullscreen"!==this.canvasSize&&"auto"!==this.canvasSize||(this._resizeHandler=()=>this.setCanvasSize(this.canvasSize),window.addEventListener("resize",this._resizeHandler)),this.context.imageSmoothingEnabled=!0,this.context.imageSmoothingQuality="high")}setCanvasSize(t){if(this.canvas&&this.containerElement){if("auto"===t){const t=this.containerElement;let e=t.clientWidth-40,i=t.clientHeight-40;if(e<=40||i<=40){let s=t.parentElement;for(;s&&(s.clientWidth<=40||s.clientHeight<=40)&&(s=s.parentElement,!s||!(s.classList.contains("modal-body")||s.classList.contains("card-body")||s.classList.contains("dialog-body")||"MAIN"===s.tagName||"BODY"===s.tagName)););s&&(e=s.clientWidth-80,i=s.clientHeight-80)}if(e>100&&i>100){let t,s;if(this.image){const a=this.image.naturalWidth/this.image.naturalHeight;a>e/i?(t=e,s=e/a):(s=i,t=i*a),t=Math.max(300,Math.floor(t)),s=Math.max(200,Math.floor(s))}else t=Math.min(600,Math.max(300,e)),s=Math.min(450,Math.max(200,i));return void this.applyCanvasSize(t,s)}}super.setCanvasSize(t)}}applyCanvasSize(t,e){if(Math.abs(t-this.canvasWidth)<10&&Math.abs(e-this.canvasHeight)<10)return;const i=window.devicePixelRatio||1;this.canvasWidth=t,this.canvasHeight=e,this.canvas.width=t*i,this.canvas.height=e*i,this.canvas.style.width=t+"px",this.canvas.style.height=e+"px",this.context.setTransform(i,0,0,i,0,0),this.isLoaded&&this.renderCanvas()}async loadImage(t){await super.loadImage(t),this.renderCanvas()}renderImage(){if(!this.image)return;this.context.filter=this.getFilterString();const t=Math.min(this.canvasWidth/this.image.naturalWidth,this.canvasHeight/this.image.naturalHeight),e=this.image.naturalWidth*t,i=this.image.naturalHeight*t,s=(this.canvasWidth-e)/2,a=(this.canvasHeight-i)/2;this.context.drawImage(this.image,s,a,e,i),this.context.filter="none"}getCombinedFilters(){const t={...this.filters};if("none"!==this.currentPreset&&this.presetEffects[this.currentPreset]){const e=this.presetEffects[this.currentPreset].filters;e&&Object.assign(t,e)}return t}getFilterString(){const t=this.getCombinedFilters();return this.hasFilters()||"none"!==this.currentPreset?[`brightness(${t.brightness}%)`,`contrast(${t.contrast}%)`,`saturate(${t.saturation}%)`,`hue-rotate(${t.hue}deg)`,`blur(${t.blur}px)`,`grayscale(${t.grayscale}%)`,`sepia(${t.sepia}%)`].join(" "):"none"}hasFilters(){return 100!==this.filters.brightness||100!==this.filters.contrast||100!==this.filters.saturation||0!==this.filters.hue||0!==this.filters.blur||0!==this.filters.grayscale||0!==this.filters.sepia}async onPassThruActionResetFilters(){this.resetFilters()}async onPassThruActionApplyPreset(t,e){t.preventDefault();const i=e.getAttribute("data-preset");i&&this.presetEffects[i]&&this.applyPreset(i)}async onPassThruActionPreviewOriginal(t,e){if("true"===e.dataset.previewing){this.context.clearRect(0,0,this.canvasWidth,this.canvasHeight),this.context.filter="none";const t=(this.canvasWidth-this.image.naturalWidth)/2,e=(this.canvasHeight-this.image.naturalHeight)/2;this.context.drawImage(this.image,t,e)}else this.renderCanvas()}async onChangeFilterChange(t,e){const i=e.getAttribute("data-filter"),s=parseFloat(e.value);this.updateFilter(i,s)}updateFilter(t,e){if(!(t in this.filters))return;const i=this.filters[t];this.filters[t]=e,this.updateFilterDisplay(t,e),this.renderCanvas(),this.emitFilterEvent("filter-changed",{filter:t,oldValue:i,newValue:e,allFilters:{...this.filters}})}updateFilterDisplay(t,e){const i=this.element.querySelector(`[data-filter="${t}"].filter-value`);if(i){const s="hue"===t?"°":"blur"===t?"px":"%";i.textContent=`${e}${s}`}}resetFilters(){const t={...this.filters},e=this.currentPreset;this.filters={brightness:100,contrast:100,saturation:100,hue:0,blur:0,grayscale:0,sepia:0},this.currentPreset="none",this.updateAllFilterInputs(),this.renderCanvas(),this.emitFilterEvent("filters-reset",{oldFilters:t,newFilters:{...this.filters},oldPreset:e,newPreset:this.currentPreset})}updateAllFilterInputs(){Object.keys(this.filters).forEach(t=>{const e=this.element.querySelector(`[data-filter="${t}"][type="range"]`);e&&(e.value=this.filters[t],this.updateFilterDisplay(t,this.filters[t]))})}applyPreset(t){this.presetEffects[t]&&(this.presetEffects[t],this.currentPreset=t,this.currentPreset=t,this.renderCanvas(),this.emitFilterEvent("preset-applied",{preset:t,filters:{...this.filters}}))}getFilterState(){return{...this.filters}}setFilterState(t){this.filters={...this.filters,...t},this.updateAllFilterInputs(),this.renderCanvas(),this.emitFilterEvent("filters-set",{filters:{...this.filters}})}exportFilteredImageData(){return this.canvas?this.exportImageData():null}async exportFilteredImageBlob(t=.9){return this.canvas?this.exportImageBlob(t):null}createFilteredCanvas(){if(!this.image)return null;const t=document.createElement("canvas"),e=t.getContext("2d");return t.width=this.image.naturalWidth,t.height=this.image.naturalHeight,e.filter=this.getFilterString(),e.drawImage(this.image,0,0),e.filter="none",t}emitFilterEvent(t,e={}){const i=this.getApp()?.events;i&&i.emit(`imagefilters:${t}`,{view:this,hasFilters:this.hasFilters(),filterString:this.getFilterString(),...e})}handleImageLoad(){super.handleImageLoad(),this.emitFilterEvent("ready",{filters:{...this.filters}})}async onBeforeDestroy(){await super.onBeforeDestroy(),this._resizeHandler&&window.removeEventListener("resize",this._resizeHandler),this.emitFilterEvent("destroyed")}static async showDialog(t,i={}){const{title:s="Apply Filters",alt:a="Image",size:n="xl",showControls:o=!0,allowReset:r=!0,showPresets:l=!0,showBasicControls:h=!0,showAdvancedControls:c=!0,controlsInDropdowns:d=!0,canvasSize:m="auto",autoFit:g=!0,crossOrigin:u="anonymous",...p}=i,v=new ImageFiltersView({imageUrl:t,alt:a,title:s,canvasSize:m,autoFit:g,crossOrigin:u,showControls:o,allowReset:r,showPresets:l,showBasicControls:h,showAdvancedControls:c,controlsInDropdowns:d}),w=new e.ModalView({title:s,body:v,size:n,centered:!0,backdrop:"static",keyboard:!0,noBodyPadding:!0,buttons:[{text:"Cancel",action:"cancel",class:"btn btn-secondary",dismiss:!0},{text:"Apply Filters",action:"apply-filters",class:"btn btn-primary"}],...p});return await w.render(!0,document.body),w.show(),new Promise(t=>{w.on("hidden",()=>{w.destroy(),t({action:"cancel",view:v})}),w.on("action:cancel",()=>{w.hide()}),w.on("action:apply-filters",async()=>{const e=v.exportFilteredImageData();w.hide(),t({action:"filters",view:v,data:e,filterState:v.getFilterState()})})})}}window.ImageFiltersView=ImageFiltersView;class ImageEditor extends t.View{constructor(t={}){super({...t,className:`image-editor ${t.className||""}`,tagName:"div"}),this.imageUrl=t.imageUrl||t.src||"",this.alt=t.alt||"Image",this.title=t.title||"",this.currentImageData=null,this.currentMode=t.startMode||"transform",this.history=[],this.historyIndex=-1,this.maxHistory=t.maxHistory||20,this.showToolbar=!1!==t.showToolbar,this.allowTransform=!1!==t.allowTransform,this.allowCrop=!1!==t.allowCrop,this.allowFilters=!1!==t.allowFilters,this.allowExport=!1!==t.allowExport,this.allowHistory=!1!==t.allowHistory,this.cropOptions=t.cropOptions||{},this.currentView=null,this.isInitialized=!1}async getTemplate(){return'\n <div class="image-editor-container d-flex flex-column h-100">\n {{#showToolbar}}\n \x3c!-- Toolbar --\x3e\n <div class="image-editor-toolbar bg-light border-bottom p-3" data-container="toolbar">\n <div class="d-flex justify-content-between align-items-center">\n \x3c!-- Mode Buttons --\x3e\n <div class="btn-group" role="group" aria-label="Editing modes">\n {{#allowTransform}}\n <button type="button" class="btn btn-outline-primary mode-btn"\n data-action="switch-mode" data-mode="transform"\n title="Transform: Zoom, Pan, Rotate">\n <i class="bi bi-arrows-move"></i> Transform\n </button>\n {{/allowTransform}}\n\n {{#allowCrop}}\n <button type="button" class="btn btn-outline-primary mode-btn"\n data-action="switch-mode" data-mode="crop"\n title="Crop: Select and crop image">\n <i class="bi bi-crop"></i> Crop\n </button>\n {{/allowCrop}}\n\n {{#allowFilters}}\n <button type="button" class="btn btn-outline-primary mode-btn"\n data-action="switch-mode" data-mode="filters"\n title="Filters: Brightness, Contrast, Effects">\n <i class="bi bi-palette"></i> Filters\n </button>\n {{/allowFilters}}\n </div>\n\n \x3c!-- Action Buttons --\x3e\n <div class="btn-group" role="group" aria-label="Actions">\n {{#allowHistory}}\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="undo" title="Undo" disabled>\n <i class="bi bi-arrow-counterclockwise"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="redo" title="Redo" disabled>\n <i class="bi bi-arrow-clockwise"></i>\n </button>\n {{/allowHistory}}\n\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="reset" title="Reset All Changes">\n <i class="bi bi-arrow-repeat"></i>\n </button>\n\n {{#allowExport}}\n <button type="button" class="btn btn-success btn-sm"\n data-action="export" title="Export Image">\n <i class="bi bi-download"></i> Export\n </button>\n {{/allowExport}}\n </div>\n </div>\n\n\n </div>\n {{/showToolbar}}\n\n \x3c!-- Main editing area where child views will be mounted --\x3e\n <div class="image-editor-workspace flex-grow-1 position-relative" data-container="image-workspace">\n \x3c!-- Child views will be added here dynamically --\x3e\n </div>\n\n \x3c!-- Status bar --\x3e\n <div class="image-editor-status bg-light border-top p-2" data-container="status">\n <div class="d-flex justify-content-between align-items-center">\n <small class="text-muted">\n Mode: <span class="current-mode fw-bold">Transform</span>\n </small>\n <small class="text-muted">\n <span class="image-info">Ready</span>\n </small>\n </div>\n </div>\n </div>\n '}async onAfterRender(){this.toolbarElement=this.element.querySelector(".image-editor-toolbar"),this.workspaceElement=this.element.querySelector(".image-editor-workspace"),this.statusElement=this.element.querySelector(".image-editor-status"),this.setupChildViewEvents(),await this.switchMode(this.currentMode,!0),this.saveState(),this.isInitialized=!0}createChildView(t){const e=this.currentImageData||this.imageUrl;this.currentImageData;const i={parent:this,containerId:"image-workspace",imageUrl:e,alt:this.alt,title:this.title};switch(t){case"transform":return this.allowTransform?new ImageTransformView({...i,allowPan:!0,allowZoom:!0,allowRotate:!0}):null;case"crop":return this.allowCrop?new ImageCropView({...i,showGrid:!0,minCropSize:50,...this.cropOptions}):null;case"filters":return this.allowFilters?new ImageFiltersView({...i,showControls:!0,allowReset:!0}):null;default:return null}}setupChildViewEvents(){const t=this.getApp()?.events;t&&(t.on("imagetransform:scale-changed",()=>this.saveState()),t.on("imagetransform:rotated",()=>this.saveState()),t.on("imagetransform:reset",()=>this.saveState()),t.on("imagecrop:crop-applied",t=>{const e=this.getCurrentImageData();e&&(this.currentImageData=e),this.saveState(),this.updateStatus("Crop applied successfully"),this.updateHistoryButtons()}),t.on("imagefilters:filter-changed",()=>{this.saveState(),this.updateStatus("Filter applied")}),t.on("imagefilters:filters-reset",()=>{this.saveState(),this.updateStatus("Filters reset")}))}async handleActionSwitchMode(t,e){const i=e.getAttribute("data-mode");await this.switchMode(i)}async handleActionUndo(){this.undo()}async handleActionRedo(){this.redo()}async handleActionReset(){await this.resetAll()}async handleActionExport(){await this.exportImage()&&this.updateStatus("Image exported successfully")}async switchMode(t,e=!1){if(t===this.currentMode&&!e)return;if(this.currentView&&!e){const t=this.getCurrentImageData();t?(this.currentMode,this.currentImageData=t):this.currentMode}this.currentView&&(await this.currentView.destroy(),this.currentView=null),this.element.querySelectorAll(".mode-btn").forEach(e=>{e.classList.remove("active"),e.getAttribute("data-mode")===t&&e.classList.add("active")}),this.currentMode=t,this.currentView=this.createChildView(t),this.currentView&&(await this.currentView.render(),"crop"===t&&this.currentView.startCropMode?(this.currentView.startCropMode(),this.updateStatus("Click and drag to select crop area")):"transform"===t?this.updateStatus("Use controls to transform the image"):"filters"===t&&this.updateStatus("Adjust filters to enhance the image")),this.updateCurrentModeDisplay();const i=this.getApp()?.events;i&&i.emit("imageeditor:mode-changed",{editor:this,mode:t,currentView:this.currentView})}updateCurrentModeDisplay(){const t=this.element.querySelector(".current-mode");t&&(t.textContent=this.currentMode.charAt(0).toUpperCase()+this.currentMode.slice(1))}updateStatus(t){const e=this.element.querySelector(".image-info");e&&(e.textContent=t)}saveState(){if(!this.isInitialized)return;const t={mode:this.currentMode,transform:this.currentView?.getTransformState?.(),filters:this.currentView?.getFilterState?.(),imageData:this.currentImageData,timestamp:Date.now()};this.history=this.history.slice(0,this.historyIndex+1),this.history.push(t),this.historyIndex=this.history.length-1,this.history.length>this.maxHistory&&(this.history.shift(),this.historyIndex--),this.updateHistoryButtons()}undo(){this.historyIndex>0&&(this.historyIndex--,this.restoreState(this.history[this.historyIndex]))}redo(){this.historyIndex<this.history.length-1&&(this.historyIndex++,this.restoreState(this.history[this.historyIndex]))}async restoreState(t){t.imageData&&(this.currentImageData=t.imageData),await this.switchMode(t.mode,!0),this.currentView&&(t.transform&&this.currentView.setTransformState&&this.currentView.setTransformState(t.transform),t.filters&&this.currentView.setFilterState&&this.currentView.setFilterState(t.filters)),this.updateHistoryButtons(),this.updateStatus(`Restored to ${t.mode} mode`)}updateHistoryButtons(){const t=this.element.querySelector('[data-action="undo"]'),e=this.element.querySelector('[data-action="redo"]');t&&(t.disabled=this.historyIndex<=0),e&&(e.disabled=this.historyIndex>=this.history.length-1)}async resetAll(){this.currentImageData=null,this.currentView&&this.currentView.reset&&this.currentView.reset(),await this.switchMode("transform",!0),this.history=[],this.historyIndex=-1,this.saveState(),this.updateStatus("All changes reset")}async exportImage(t={}){if(!this.currentView)return null;try{let e=null;if(e=this.getCurrentImageData(),e){const i=this.getExportFilename();if(!1!==t.download){const t=document.createElement("a");t.download=i,t.href=e,document.body.appendChild(t),t.click(),document.body.removeChild(t)}const s=this.getApp()?.events;return s&&s.emit("imageeditor:exported",{editor:this,imageData:e,filename:i}),{imageData:e,filename:i}}}catch(e){console.error("Export failed:",e),this.updateStatus("Export failed")}return null}getExportFilename(){return`edited-image-${/* @__PURE__ */(new Date).toISOString().slice(0,19).replace(/[:\-]/g,"")}.png`}async setImage(t,e="",i=""){this.imageUrl=t,this.alt=e,this.title=i,this.currentImageData=null,this.currentView&&this.currentView.setImage&&this.currentView.setImage(t,e,i),await this.resetAll()}getCurrentImageData(){if(!this.currentView)return null;let t=null;return this.currentView.exportImageData?t=this.currentView.exportImageData():this.currentView.exportFilteredImageData&&(t=this.currentView.exportFilteredImageData()),t||null}async onBeforeDestroy(){this.currentView&&(await this.currentView.destroy(),this.currentView=null);const t=this.getApp()?.events;t&&t.emit("imageeditor:destroyed",{editor:this})}static async showDialog(t,i={}){const{title:s="Image Editor",alt:a="Image",size:n="fullscreen",showToolbar:o=!0,allowTransform:r=!0,allowCrop:l=!0,allowFilters:h=!0,allowExport:c=!0,startMode:d="transform",download:m=!0,saveText:g="Export & Close",cropOptions:u,...p}=i,v=new ImageEditor({imageUrl:t,alt:a,title:s,showToolbar:o,allowTransform:r,allowCrop:l,allowFilters:h,allowExport:c,startMode:d,cropOptions:u}),w=new e.ModalView({title:s,body:v,size:n,centered:!0,backdrop:"static",keyboard:!0,buttons:[{text:"Cancel",action:"cancel",class:"btn btn-secondary",dismiss:!0},{text:g,action:"export-close",class:"btn btn-primary"}],...p});return await w.render(!0,document.body),w.show(),new Promise(t=>{let e=!1;w.on("action:cancel",()=>{w.hide()}),w.on("action:export-close",async()=>{const i=await v.exportImage({download:m});e=!0,w.hide(),t({action:"export",editor:v,data:i?.imageData,filename:i?.filename})}),w.on("hidden",()=>{w.destroy(),e||t({action:"cancel",editor:v})})})}}window.ImageEditor=ImageEditor;class ImageUploadView extends t.View{constructor(t={}){super({...t,className:`image-upload-view ${t.className||""}`,tagName:"div"}),this.autoUpload=t.autoUpload||!1,this.acceptedTypes=t.acceptedTypes||["image/jpeg","image/png","image/gif","image/webp"],this.maxFileSize=t.maxFileSize||10485760,this.uploadUrl=t.uploadUrl||null,this.onUpload=t.onUpload||null,this.selectedFile=null,this.isUploading=!1,this.previewUrl=null,this._handleDragOver=this.handleDragOver.bind(this),this._handleDragLeave=this.handleDragLeave.bind(this),this._handleDrop=this.handleDrop.bind(this),this._handleFileSelect=this.handleFileSelect.bind(this),this._preventDefaults=this.preventDefaults.bind(this)}async getTemplate(){return`\n <div class="image-upload-container">\n \x3c!-- Drop Zone --\x3e\n <div class="upload-drop-zone border-2 border-dashed rounded p-4 text-center position-relative" \n style="border-color: #dee2e6; min-height: 200px; transition: all 0.2s ease;">\n \n \x3c!-- Default State --\x3e\n <div class="upload-prompt">\n <i class="bi bi-cloud-upload text-muted" style="font-size: 3rem;"></i>\n <h5 class="mt-3 text-muted">Drop your image here</h5>\n <p class="text-muted mb-3">or</p>\n <button type="button" class="btn btn-outline-primary" data-action="select-file">\n <i class="bi bi-folder2-open"></i> Choose File\n </button>\n <input type="file" class="upload-file-input d-none" accept="image/*" multiple="false">\n <div class="mt-3">\n <small class="text-muted">Supported: JPEG, PNG, GIF, WebP (max ${Math.round(this.maxFileSize/1024/1024)}MB)</small>\n </div>\n </div>\n \n \x3c!-- Preview State --\x3e\n <div class="upload-preview d-none">\n <div class="preview-image-container mb-3">\n <img class="preview-image img-fluid rounded shadow-sm" style="max-height: 300px; max-width: 100%;">\n </div>\n <div class="preview-info">\n <div class="file-name fw-bold mb-2 text-truncate"></div>\n <div class="file-details text-muted small mb-3"></div>\n <div class="upload-actions">\n {{#autoUpload}}\n <button type="button" class="btn btn-outline-secondary" data-action="clear">\n <i class="bi bi-x"></i> Clear\n </button>\n {{/autoUpload}}\n {{^autoUpload}}\n <button type="button" class="btn btn-success me-2" data-action="upload">\n <i class="bi bi-cloud-arrow-up"></i> Upload\n </button>\n <button type="button" class="btn btn-outline-secondary" data-action="clear">\n <i class="bi bi-x"></i> Clear\n </button>\n {{/autoUpload}}\n </div>\n </div>\n </div>\n \n \x3c!-- Loading State --\x3e\n <div class="upload-loading d-none">\n <div class="spinner-border text-primary mb-3" role="status">\n <span class="visually-hidden">Uploading...</span>\n </div>\n <div class="upload-progress">\n <div class="progress mb-2" style="height: 8px;">\n <div class="progress-bar progress-bar-striped progress-bar-animated" \n role="progressbar" style="width: 0%"></div>\n </div>\n <small class="text-muted upload-status">Uploading...</small>\n </div>\n </div>\n </div>\n \n \x3c!-- Upload Result --\x3e\n <div class="upload-result mt-3 d-none">\n <div class="alert" role="alert"></div>\n </div>\n </div>\n `}async onAfterRender(){this.dropZone=this.element.querySelector(".upload-drop-zone"),this.fileInput=this.element.querySelector(".upload-file-input"),this.promptElement=this.element.querySelector(".upload-prompt"),this.previewElement=this.element.querySelector(".upload-preview"),this.loadingElement=this.element.querySelector(".upload-loading"),this.resultElement=this.element.querySelector(".upload-result"),this.previewImage=this.element.querySelector(".preview-image"),this.fileName=this.element.querySelector(".file-name"),this.fileDetails=this.element.querySelector(".file-details"),this.progressBar=this.element.querySelector(".progress-bar"),this.uploadStatus=this.element.querySelector(".upload-status"),this.setupEventListeners()}setupEventListeners(){this.dropZone.addEventListener("dragenter",this._preventDefaults),this.dropZone.addEventListener("dragover",this._handleDragOver),this.dropZone.addEventListener("dragleave",this._handleDragLeave),this.dropZone.addEventListener("drop",this._handleDrop),this.fileInput.addEventListener("change",this._handleFileSelect),["dragenter","dragover","dragleave","drop"].forEach(t=>{document.addEventListener(t,this._preventDefaults)})}preventDefaults(t){t.preventDefault(),t.stopPropagation()}handleDragOver(t){this.preventDefaults(t),this.dropZone.classList.add("border-primary","bg-light"),this.dropZone.style.borderColor="#0d6efd"}handleDragLeave(t){this.preventDefaults(t),this.dropZone.contains(t.relatedTarget)||(this.dropZone.classList.remove("border-primary","bg-light"),this.dropZone.style.borderColor="#dee2e6")}async handleDrop(t){this.preventDefaults(t),this.dropZone.classList.remove("border-primary","bg-light"),this.dropZone.style.borderColor="#dee2e6";const e=Array.from(t.dataTransfer.files);e.length>0&&await this.processFile(e[0])}async handleFileSelect(t){const e=Array.from(t.target.files);e.length>0&&await this.processFile(e[0])}async processFile(t){const e=this.validateFile(t);e.valid?(this.selectedFile=t,await this.showPreview(t),this.autoUpload&&setTimeout(()=>this.uploadFile(),100)):this.showError(e.error)}validateFile(t){return this.acceptedTypes.includes(t.type)?t.size>this.maxFileSize?{valid:!1,error:`File size (${this.formatFileSize(t.size)}) exceeds maximum allowed size (${this.formatFileSize(this.maxFileSize)})`}:{valid:!0}:{valid:!1,error:`File type "${t.type}" is not supported. Please use: ${this.acceptedTypes.map(t=>t.split("/")[1].toUpperCase()).join(", ")}`}}async showPreview(t){this.previewUrl&&URL.revokeObjectURL(this.previewUrl),this.previewUrl=URL.createObjectURL(t),this.previewImage.src=this.previewUrl,this.fileName.textContent=t.name,this.fileDetails.textContent=`${this.formatFileSize(t.size)} • ${t.type.split("/")[1].toUpperCase()}`,this.promptElement.classList.add("d-none"),this.previewElement.classList.remove("d-none"),this.hideResult(),this.emitUploadEvent("preview",{file:t,previewUrl:this.previewUrl})}async uploadFile(){if(this.selectedFile&&!this.isUploading){this.isUploading=!0,this.showLoading();try{let t;if(this.onUpload&&"function"==typeof this.onUpload)t=await this.onUpload(this.selectedFile,this.updateProgress.bind(this));else{if(!this.uploadUrl)throw new Error("No upload method configured. Provide either uploadUrl or onUpload callback.");t=await this.uploadToUrl(this.selectedFile)}this.showSuccess("File uploaded successfully!"),this.emitUploadEvent("upload-success",{file:this.selectedFile,result:t})}catch(t){console.error("Upload failed:",t),this.showError(`Upload failed: ${t.message}`),this.emitUploadEvent("upload-error",{file:this.selectedFile,error:t})}finally{this.isUploading=!1,this.hideLoading()}}}async uploadToUrl(t){return new Promise((e,i)=>{const s=new FormData;s.append("image",t);const a=new XMLHttpRequest;a.upload.addEventListener("progress",t=>{if(t.lengthComputable){const e=Math.round(t.loaded/t.total*100);this.updateProgress(e)}}),a.addEventListener("load",()=>{if(a.status>=200&&a.status<300)try{const t=JSON.parse(a.responseText);e(t)}catch(t){e({success:!0,response:a.responseText})}else i(new Error(`HTTP ${a.status}: ${a.statusText}`))}),a.addEventListener("error",()=>{i(new Error("Network error occurred"))}),a.addEventListener("timeout",()=>{i(new Error("Upload timeout"))}),a.open("POST",this.uploadUrl),a.timeout=3e4,a.send(s)})}updateProgress(t){this.progressBar&&(this.progressBar.style.width=`${t}%`,this.progressBar.setAttribute("aria-valuenow",t)),this.uploadStatus&&(this.uploadStatus.textContent=`Uploading... ${t}%`)}showLoading(){this.previewElement.classList.add("d-none"),this.loadingElement.classList.remove("d-none"),this.updateProgress(0)}hideLoading(){this.loadingElement.classList.add("d-none"),this.autoUpload&&!this.selectedFile||this.previewElement.classList.remove("d-none")}showSuccess(t){this.showResult("success",t)}showError(t){this.showResult("danger",t)}showResult(t,e){const i=this.resultElement.querySelector(".alert"),s="success"===t?"check-circle-fill":"exclamation-triangle-fill";i.className=`alert alert-${t}`,i.innerHTML=`\n <i class="bi bi-${s} me-2"></i>\n ${e}\n `,this.resultElement.classList.remove("d-none"),"success"===t&&setTimeout(()=>this.hideResult(),5e3)}hideResult(){this.resultElement.classList.add("d-none")}clearFile(){this.previewUrl&&(URL.revokeObjectURL(this.previewUrl),this.previewUrl=null),this.selectedFile=null,this.isUploading=!1,this.fileInput.value="",this.previewElement.classList.add("d-none"),this.loadingElement.classList.add("d-none"),this.promptElement.classList.remove("d-none"),this.hideResult(),this.emitUploadEvent("cleared")}formatFileSize(t){if(0===t)return"0 Bytes";const e=Math.floor(Math.log(t)/Math.log(1024));return parseFloat((t/Math.pow(1024,e)).toFixed(2))+" "+["Bytes","KB","MB","GB"][e]}emitUploadEvent(t,e={}){const i=this.getApp()?.events;i&&i.emit(`imageupload:${t}`,{view:this,...e})}async handleActionSelectFile(){this.fileInput.click()}async handleActionUpload(){await this.uploadFile()}async handleActionClear(){this.clearFile()}async onBeforeDestroy(){this.previewUrl&&(URL.revokeObjectURL(this.previewUrl),this.previewUrl=null),this.dropZone&&(this.dropZone.removeEventListener("dragenter",this._preventDefaults),this.dropZone.removeEventListener("dragover",this._handleDragOver),this.dropZone.removeEventListener("dragleave",this._handleDragLeave),this.dropZone.removeEventListener("drop",this._handleDrop)),this.fileInput&&this.fileInput.removeEventListener("change",this._handleFileSelect),["dragenter","dragover","dragleave","drop"].forEach(t=>{document.removeEventListener(t,this._preventDefaults)}),this.emitUploadEvent("destroyed")}}window.ImageUploadView=ImageUploadView;class LightboxGallery extends t.View{constructor(t={}){super({...t,className:`lightbox-gallery ${t.className||""}`,tagName:"div"});const e=Array.isArray(t.images)?t.images:[t.images||t.src].filter(Boolean);this.images=e.map(t=>"string"==typeof t?{src:t,alt:""}:{src:t.src,alt:t.alt||""}),this.currentIndex=t.startIndex||0,this.showNavigation=!1!==t.showNavigation&&this.images.length>1,this.showCounter=!1!==t.showCounter&&this.images.length>1,this.allowKeyboard=!1!==t.allowKeyboard,this.closeOnBackdrop=!1!==t.closeOnBackdrop,this.fitToScreen=!1!==t.fitToScreen,this._keyboardHandler=this.handleKeyboard.bind(this),this.updateTemplateProperties()}updateTemplateProperties(){this.currentImage=this.images[this.currentIndex]||{src:"",alt:""},this.currentNumber=this.currentIndex+1,this.total=this.images.length,this.isFirst=0===this.currentIndex,this.isLast=this.currentIndex===this.images.length-1,this.imageStyle=this.fitToScreen?"width: 90vw; max-height: 100%; object-fit: contain; user-select: none; cursor: zoom-in;":"max-width: none; max-height: none; user-select: none; cursor: zoom-out;",this.containerStyle=this.fitToScreen?"":"overflow: auto;",this.modeIndicator=this.fitToScreen?"Fit to Screen":"Original Size"}async getTemplate(){return this.images[this.currentIndex],this.images.length,'\n <div class="lightbox-overlay position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"\n style="background: rgba(0,0,0,0.9); z-index: 9999;"\n data-action="backdrop-click">\n\n \x3c!-- Close button --\x3e\n <button type="button" class="btn-close btn-close-white position-absolute top-0 end-0 m-4"\n data-action="close"\n style="z-index: 10001;"\n title="Close"></button>\n\n \x3c!-- Counter --\x3e\n {{#showCounter}}\n <div class="lightbox-counter position-absolute top-0 start-50 translate-middle-x mt-4 text-white"\n style="z-index: 10001; font-size: 1.1rem;">\n {{currentNumber}} of {{total}}\n </div>\n {{/showCounter}}\n\n \x3c!-- Mode Indicator --\x3e\n <div class="lightbox-mode-indicator position-absolute bottom-0 start-50 translate-middle-x mb-4 text-white bg-dark bg-opacity-75 px-3 py-2 rounded"\n style="z-index: 10001; font-size: 0.9rem;">\n {{modeIndicator}} • Click image to toggle\n </div>\n\n \x3c!-- Navigation --\x3e\n {{#showNavigation}}\n <button type="button" class="btn btn-light btn-lg position-absolute start-0 top-50 translate-middle-y ms-4"\n data-action="prev"\n style="z-index: 10001;"\n title="Previous"\n {{#isFirst}}disabled{{/isFirst}}>\n <i class="bi bi-chevron-left"></i>\n </button>\n\n <button type="button" class="btn btn-light btn-lg position-absolute end-0 top-50 translate-middle-y me-4"\n data-action="next"\n style="z-index: 10001;"\n title="Next"\n {{#isLast}}disabled{{/isLast}}>\n <i class="bi bi-chevron-right"></i>\n </button>\n {{/showNavigation}}\n\n \x3c!-- Image container --\x3e\n <div class="lightbox-image-container w-100 h-100 d-flex align-items-center justify-content-center p-5"\n style="{{containerStyle}}">\n {{#currentImage}}\n <img src="{{src}}"\n alt="{{alt}}"\n class="lightbox-image img-fluid"\n style="{{imageStyle}}"\n data-action="image-click">\n {{/currentImage}}\n </div>\n\n \x3c!-- Loading spinner --\x3e\n <div class="lightbox-loading position-absolute top-50 start-50 translate-middle text-white"\n style="display: none;">\n <div class="spinner-border" role="status">\n <span class="visually-hidden">Loading...</span>\n </div>\n </div>\n </div>\n '}async onAfterRender(){document.body.appendChild(this.element),document.body.style.overflow="hidden",this.allowKeyboard&&document.addEventListener("keydown",this._keyboardHandler),this.preloadAdjacentImages()}async handleActionClose(){this.close()}async handleActionBackdropClick(t){this.closeOnBackdrop&&t.target===t.currentTarget&&this.close()}async handleActionPrev(){this.showPrevious()}async handleActionNext(){this.showNext()}async handleActionImageClick(){this.toggleImageMode()}showPrevious(){this.currentIndex>0&&(this.currentIndex--,this.updateImage(),this.preloadAdjacentImages())}showNext(){this.currentIndex<this.images.length-1&&(this.currentIndex++,this.updateImage(),this.preloadAdjacentImages())}goToImage(t){t>=0&&t<this.images.length&&t!==this.currentIndex&&(this.currentIndex=t,this.updateImage(),this.preloadAdjacentImages())}async updateImage(){const t=this.images[this.currentIndex],e=this.element.querySelector(".lightbox-image"),i=this.element.querySelector(".lightbox-counter"),s=this.element.querySelector('[data-action="prev"]'),a=this.element.querySelector('[data-action="next"]');e&&(this.showLoading(),e.src=t.src,e.alt=t.alt,await this.waitForImageLoad(e),this.hideLoading()),i&&(i.textContent=`${this.currentIndex+1} of ${this.images.length}`),s&&(s.disabled=0===this.currentIndex),a&&(a.disabled=this.currentIndex===this.images.length-1),this.updateTemplateProperties(),this.updateImageDisplay();const n=this.getApp()?.events;n&&n.emit("lightbox:image-changed",{gallery:this,index:this.currentIndex,image:t})}showLoading(){const t=this.element.querySelector(".lightbox-loading");t&&(t.style.display="block")}hideLoading(){const t=this.element.querySelector(".lightbox-loading");t&&(t.style.display="none")}waitForImageLoad(t){return new Promise(e=>{t.complete?e():(t.onload=e,t.onerror=e)})}preloadAdjacentImages(){const t=[];this.currentIndex>0&&t.push(this.currentIndex-1),this.currentIndex<this.images.length-1&&t.push(this.currentIndex+1),t.forEach(t=>{const e=this.images[t];(new Image).src=e.src})}handleKeyboard(t){switch(t.key){case"Escape":t.preventDefault(),this.close();break;case"ArrowLeft":t.preventDefault(),this.showPrevious();break;case"ArrowRight":t.preventDefault(),this.showNext();break;case"Home":t.preventDefault(),this.goToImage(0);break;case"End":t.preventDefault(),this.goToImage(this.images.length-1)}}toggleImageMode(){this.fitToScreen=!this.fitToScreen,this.updateTemplateProperties(),this.updateImageDisplay();const t=this.getApp()?.events;t&&t.emit("lightbox:mode-changed",{gallery:this,fitToScreen:this.fitToScreen})}updateImageDisplay(){const t=this.element.querySelector(".lightbox-image"),e=this.element.querySelector(".lightbox-image-container"),i=this.element.querySelector(".lightbox-mode-indicator");t&&(this.fitToScreen?(t.style.maxWidth="100%",t.style.maxHeight="100%",t.style.objectFit="contain",t.style.cursor="zoom-in"):(t.style.maxWidth="none",t.style.maxHeight="none",t.style.objectFit="none",t.style.cursor="zoom-out"),t.style.userSelect="none"),e&&(e.style.overflow=this.fitToScreen?"":"auto"),i&&(i.textContent=`${this.modeIndicator} • Click image to toggle`)}close(){const t=this.getApp()?.events;t&&t.emit("lightbox:closed",{gallery:this}),this.destroy()}async onBeforeDestroy(){document.body.style.overflow="",this.allowKeyboard&&document.removeEventListener("keydown",this._keyboardHandler),this.element.parentNode===document.body&&document.body.removeChild(this.element)}static show(t,e={}){const i=new LightboxGallery({images:t,...e});return i.render().then(()=>{i.mount()}),i}}window.LightboxGallery=LightboxGallery;class PDFViewer extends t.View{constructor(t={}){super({...t,className:`pdf-viewer ${t.className||""}`,tagName:"div"}),this.pdfUrl=t.pdfUrl||t.src||"",this.title=t.title||"PDF Document",this.pdfDoc=null,this.currentPage=1,this.totalPages=0,this.pageRendering=!1,this.pageNumPending=null,this.scale=1,this.minScale=.25,this.maxScale=5,this.scaleStep=.25,this.fitMode="page",this.canvas=null,this.ctx=null,this.showControls=!1!==t.showControls,this.allowZoom=!1!==t.allowZoom,this.allowNavigation=!1!==t.allowNavigation,this.showPageNumbers=!1!==t.showPageNumbers,this.isLoaded=!1,this.isLoading=!1,this.pdfjsWorkerPath=t.pdfjsWorkerPath||"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.worker.min.js",this.pdfjsCMapUrl=t.pdfjsCMapUrl||"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/cmaps/",this.canvasContainer=null,this.controlsElement=null,this.statusElement=null,this.pageInput=null}async getTemplate(){return'\n <div class="pdf-viewer-container">\n {{#showControls}}\n <div class="pdf-viewer-toolbar" data-container="toolbar">\n <div class="btn-toolbar" role="toolbar">\n \x3c!-- Navigation Controls --\x3e\n {{#allowNavigation}}\n <div class="btn-group me-2" role="group" aria-label="Navigation">\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="first-page" title="First Page">\n <i class="bi bi-chevron-double-left"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="prev-page" title="Previous Page">\n <i class="bi bi-chevron-left"></i>\n </button>\n \n {{#showPageNumbers}}\n <div class="input-group input-group-sm" style="width: 120px;">\n <input type="number" class="form-control text-center page-input" min="1" value="1" data-change-action="page-input">\n <span class="input-group-text page-total">/ 0</span>\n </div>\n {{/showPageNumbers}}\n \n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="next-page" title="Next Page">\n <i class="bi bi-chevron-right"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="last-page" title="Last Page">\n <i class="bi bi-chevron-double-right"></i>\n </button>\n </div>\n {{/allowNavigation}}\n\n \x3c!-- Zoom Controls --\x3e\n {{#allowZoom}}\n <div class="btn-group me-2" role="group" aria-label="Zoom">\n <button type="button" class="btn btn-outline-secondary 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="zoom-in" title="Zoom In">\n <i class="bi bi-zoom-in"></i>\n </button>\n \n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" title="Fit">\n <i class="bi bi-arrows-fullscreen"></i>\n </button>\n <ul class="dropdown-menu">\n <li><a class="dropdown-item" href="#" data-action="fit-page">Fit Page</a></li>\n <li><a class="dropdown-item" href="#" data-action="fit-width">Fit Width</a></li>\n <li><a class="dropdown-item" href="#" data-action="actual-size">Actual Size</a></li>\n </ul>\n </div>\n </div>\n {{/allowZoom}}\n\n \x3c!-- Utility Controls --\x3e\n <div class="btn-group me-2" role="group" aria-label="Utilities">\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="download" title="Download">\n <i class="bi bi-download"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary btn-sm" data-action="print" title="Print">\n <i class="bi bi-printer"></i>\n </button>\n </div>\n </div>\n </div>\n {{/showControls}}\n\n \x3c!-- PDF Content Area --\x3e\n <div class="pdf-viewer-content" data-container="content">\n <div class="pdf-canvas-container" data-container="canvasContainer">\n <canvas class="pdf-canvas" data-container="canvas"></canvas>\n </div>\n\n <div class="pdf-viewer-overlay">\n <div class="pdf-viewer-loading">\n <div class="spinner-border text-primary" role="status">\n <span class="visually-hidden">Loading PDF...</span>\n </div>\n <div class="mt-2">Loading PDF...</div>\n </div>\n </div>\n\n <div class="pdf-viewer-error" style="display: none;">\n <div class="alert alert-danger" role="alert">\n <i class="bi bi-exclamation-triangle"></i>\n <strong>Error:</strong> <span class="error-message">Failed to load PDF</span>\n </div>\n </div>\n </div>\n\n \x3c!-- Status Bar --\x3e\n <div class="pdf-viewer-status" data-container="status">\n <small class="text-muted">\n <span class="current-page">0</span> of <span class="total-pages">0</span> pages |\n <span class="zoom-level">100%</span> |\n <span class="document-title">{{title}}</span>\n </small>\n </div>\n </div>\n '}get(){return{pdfUrl:this.pdfUrl,title:this.title,showControls:this.showControls,allowZoom:this.allowZoom,allowNavigation:this.allowNavigation,showPageNumbers:this.showPageNumbers}}async onAfterRender(){this.canvas=this.element.querySelector(".pdf-canvas"),this.canvasContainer=this.element.querySelector(".pdf-canvas-container"),this.controlsElement=this.element.querySelector(".pdf-viewer-toolbar"),this.statusElement=this.element.querySelector(".pdf-viewer-status"),this.pageInput=this.element.querySelector(".page-input"),this.overlayElement=this.element.querySelector(".pdf-viewer-overlay"),this.errorElement=this.element.querySelector(".pdf-viewer-error"),this.canvas&&(this.ctx=this.canvas.getContext("2d")),this.setupEssentialEventListeners(),await this.initializePDFJS(),this.pdfUrl&&await this.loadPDF()}setupEssentialEventListeners(){const t=t=>this.handleKeyDown(t);document.addEventListener("keydown",t);let e=null;this.canvasContainer&&(e=new ResizeObserver(()=>{"auto"!==this.fitMode&&this.applyFitMode()}),e.observe(this.canvasContainer)),this._essentialListeners=[{el:document,type:"keydown",fn:t}],this._resizeObserver=e}async initializePDFJS(){try{return void 0===window.pdfjsLib&&await this.loadPDFJSLibrary(),window.pdfjsLib.GlobalWorkerOptions.workerSrc=this.pdfjsWorkerPath,!0}catch(t){return console.error("Failed to initialize PDF.js:",t),this.showError("Failed to initialize PDF viewer"),!1}}async loadPDFJSLibrary(){return new Promise((t,e)=>{const i=document.createElement("script");i.src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.min.js",i.onload=t,i.onerror=e,document.head.appendChild(i)})}async handleActionFirstPage(){await this.goToPage(1)}async handleActionPrevPage(){await this.goToPage(this.currentPage-1)}async handleActionNextPage(){await this.goToPage(this.currentPage+1)}async handleActionLastPage(){await this.goToPage(this.totalPages)}async onChangePageInput(t,e){const i=parseInt(e.value,10);i>=1&&i<=this.totalPages?await this.goToPage(i):e.value=this.currentPage}async handleActionZoomIn(){this.setScale(this.scale+this.scaleStep)}async handleActionZoomOut(){this.setScale(this.scale-this.scaleStep)}async handleActionFitPage(){this.setFitMode("page")}async handleActionFitWidth(){this.setFitMode("width")}async handleActionActualSize(){this.setScale(1),this.fitMode="auto"}async handleActionDownload(){this.downloadPDF()}async handleActionPrint(){window.print()}async loadPDF(){if(!this.pdfUrl||!window.pdfjsLib)return this.showError("PDF URL or PDF.js library not available"),!1;this.isLoading=!0,this.showLoading();try{const t=window.pdfjsLib.getDocument({url:this.pdfUrl,cMapUrl:this.pdfjsCMapUrl,cMapPacked:!0});this.pdfDoc=await t.promise,this.totalPages=this.pdfDoc.numPages,this.currentPage=1,this.updatePageControls(),this.updateStatus(),await this.renderPage(1),this.isLoaded=!0,this.isLoading=!1,this.element.classList.add("loaded"),this.hideLoading(),this.applyFitMode();const e=this.getApp()?.events;return e&&e.emit("pdfviewer:loaded",{viewer:this,pdfUrl:this.pdfUrl,totalPages:this.totalPages}),!0}catch(t){console.error("Error loading PDF:",t),this.isLoading=!1,this.showError("Failed to load PDF document");const e=this.getApp()?.events;return e&&e.emit("pdfviewer:error",{viewer:this,pdfUrl:this.pdfUrl,error:t.message}),!1}}async renderPage(t){if(this.pageRendering)this.pageNumPending=t;else if(this.pdfDoc&&this.canvas&&this.ctx){this.pageRendering=!0,this.currentPage=t;try{const e=await this.pdfDoc.getPage(t),i=e.getViewport({scale:this.scale});this.canvas.height=i.height,this.canvas.width=i.width,this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);const s={canvasContext:this.ctx,viewport:i},a=e.render(s);if(await a.promise,this.pageRendering=!1,null!==this.pageNumPending){const t=this.pageNumPending;this.pageNumPending=null,await this.renderPage(t)}this.updatePageControls(),this.updateStatus();const n=this.getApp()?.events;n&&n.emit("pdfviewer:page-changed",{viewer:this,currentPage:this.currentPage,totalPages:this.totalPages})}catch(e){console.error("Error rendering page:",e),this.pageRendering=!1,this.showError("Failed to render PDF page")}}}async goToPage(t){!this.pdfDoc||t<1||t>this.totalPages||await this.renderPage(t)}setScale(t){const e=this.scale;this.scale=Math.max(this.minScale,Math.min(this.maxScale,t)),this.fitMode="auto",this.isLoaded&&this.renderPage(this.currentPage);const i=this.getApp()?.events;i&&e!==this.scale&&i.emit("pdfviewer:scale-changed",{viewer:this,oldScale:e,newScale:this.scale})}setFitMode(t){const e=this.fitMode;this.fitMode=t,this.applyFitMode();const i=this.getApp()?.events;i&&i.emit("pdfviewer:fit-mode-changed",{viewer:this,oldMode:e,newMode:t})}applyFitMode(){this.isLoaded&&this.pdfDoc&&this.canvasContainer&&this.pdfDoc.getPage(this.currentPage).then(t=>{const e=this.canvasContainer.getBoundingClientRect(),i=t.getViewport({scale:1});let s;if("page"===this.fitMode){const t=(e.width-40)/i.width,a=(e.height-40)/i.height;s=Math.min(t,a)}else{if("width"!==this.fitMode)return;s=(e.width-40)/i.width}this.scale=Math.max(this.minScale,Math.min(this.maxScale,s)),this.renderPage(this.currentPage)})}handleKeyDown(t){if("INPUT"!==t.target.tagName||t.target===this.pageInput)switch(t.key){case"ArrowLeft":case"PageUp":t.preventDefault(),this.goToPage(this.currentPage-1);break;case"ArrowRight":case"PageDown":t.preventDefault(),this.goToPage(this.currentPage+1);break;case"Home":t.preventDefault(),this.goToPage(1);break;case"End":t.preventDefault(),this.goToPage(this.totalPages);break;case"+":case"=":(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.setScale(this.scale+this.scaleStep));break;case"-":(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.setScale(this.scale-this.scaleStep));break;case"0":(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.setFitMode("page"))}}updatePageControls(){this.pageInput&&(this.pageInput.value=this.currentPage);const t=this.element.querySelector(".page-total");t&&(t.textContent=`/ ${this.totalPages}`);const e=this.element.querySelector('[data-action="first-page"]'),i=this.element.querySelector('[data-action="prev-page"]'),s=this.element.querySelector('[data-action="next-page"]'),a=this.element.querySelector('[data-action="last-page"]');e&&(e.disabled=this.currentPage<=1),i&&(i.disabled=this.currentPage<=1),s&&(s.disabled=this.currentPage>=this.totalPages),a&&(a.disabled=this.currentPage>=this.totalPages);const n=this.element.querySelector('[data-action="zoom-in"]'),o=this.element.querySelector('[data-action="zoom-out"]');n&&(n.disabled=this.scale>=this.maxScale),o&&(o.disabled=this.scale<=this.minScale)}updateStatus(){if(!this.statusElement)return;const t=this.statusElement.querySelector(".current-page"),e=this.statusElement.querySelector(".total-pages"),i=this.statusElement.querySelector(".zoom-level");t&&(t.textContent=this.currentPage),e&&(e.textContent=this.totalPages),i&&(i.textContent=`${Math.round(100*this.scale)}%`)}showLoading(){this.overlayElement&&(this.overlayElement.style.display="flex")}hideLoading(){this.overlayElement&&(this.overlayElement.style.display="none")}showError(t){if(this.hideLoading(),this.errorElement){const e=this.errorElement.querySelector(".error-message");e&&(e.textContent=t),this.errorElement.style.display="block"}console.error("PDF Viewer Error:",t)}downloadPDF(){if(!this.pdfUrl)return;const t=document.createElement("a");t.href=this.pdfUrl,t.download=this.title+".pdf",t.target="_blank",t.click()}setPDF(t,e=""){const i=this.pdfUrl;this.pdfUrl=t,this.title=e||"PDF Document",this.isLoaded=!1,this.element.classList.remove("loaded"),this.pdfDoc&&(this.pdfDoc.destroy(),this.pdfDoc=null),this.currentPage=1,this.totalPages=0,this.scale=1,t&&this.loadPDF();const s=this.getApp()?.events;s&&s.emit("pdfviewer:pdf-changed",{viewer:this,oldPdfUrl:i,newPdfUrl:t})}getCurrentPage(){return this.currentPage}getTotalPages(){return this.totalPages}getCurrentScale(){return this.scale}async onBeforeDestroy(){this.pdfDoc&&(this.pdfDoc.destroy(),this.pdfDoc=null),this.pageRendering=!1,this.pageNumPending=null,this._essentialListeners&&(this._essentialListeners.forEach(({el:t,type:e,fn:i})=>{t&&t.removeEventListener(e,i)}),this._essentialListeners=null),this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null);const t=this.getApp()?.events;t&&t.emit("pdfviewer:destroyed",{viewer:this})}static async showDialog(t,i={}){const{title:s="PDF Viewer",size:a="fullscreen",showControls:n=!0,allowZoom:o=!0,allowNavigation:r=!0,showPageNumbers:l=!0,...h}=i,c=new PDFViewer({pdfUrl:t,title:s,showControls:n,allowZoom:o,allowNavigation:r,showPageNumbers:l}),d=new e.ModalView({title:s,body:c,size:a,centered:!0,backdrop:"static",keyboard:!0,buttons:[{text:"Download",action:"download",class:"btn btn-outline-primary"},{text:"Close",action:"close",class:"btn btn-secondary",dismiss:!0}],...h});return await d.render(),document.body.appendChild(d.element),await d.mount(),d.show(),new Promise(t=>{d.on("hidden",()=>{d.destroy(),t(c)}),d.on("action:download",()=>{c.downloadPDF()}),d.on("action:close",()=>{d.hide()})})}}exports.WebApp=i.WebApp,exports.BUILD_TIME=s.BUILD_TIME,exports.VERSION=s.VERSION,exports.VERSION_INFO=s.VERSION_INFO,exports.VERSION_MAJOR=s.VERSION_MAJOR,exports.VERSION_MINOR=s.VERSION_MINOR,exports.VERSION_REVISION=s.VERSION_REVISION,exports.ImageCanvasView=ImageCanvasView,exports.ImageCropView=ImageCropView,exports.ImageEditor=ImageEditor,exports.ImageFiltersView=ImageFiltersView,exports.ImageTransformView=ImageTransformView,exports.ImageUploadView=ImageUploadView,exports.ImageViewer=ImageViewer,exports.LightboxGallery=LightboxGallery,exports.PDFViewer=PDFViewer;
|
|
2
2
|
//# sourceMappingURL=lightbox.cjs.js.map
|