import { DeesElement, html, customElement, type TemplateResult, property, state, cssManager, } from '@design.estate/dees-element'; import '../../00group-utility/dees-icon/dees-icon.js'; import { demo } from './demo.js'; declare global { interface HTMLElementTagNameMap { 'dees-image-viewer': DeesImageViewer; } } @customElement('dees-image-viewer') export class DeesImageViewer extends DeesElement { public static demo = demo; public static demoGroups = ['Media']; @property() accessor src: string = ''; @property() accessor alt: string = ''; @property() accessor fit: 'contain' | 'cover' | 'actual' = 'contain'; @property({ type: Boolean }) accessor showToolbar: boolean = true; @state() accessor zoom: number = 1; @state() accessor panX: number = 0; @state() accessor panY: number = 0; @state() accessor isDragging: boolean = false; @state() accessor loading: boolean = true; @state() accessor error: string = ''; @state() accessor imageNaturalWidth: number = 0; @state() accessor imageNaturalHeight: number = 0; private dragStartX = 0; private dragStartY = 0; private dragStartPanX = 0; private dragStartPanY = 0; public render(): TemplateResult { return html`
${this.showToolbar ? html`
${this.imageNaturalWidth > 0 ? html`
${this.imageNaturalWidth} x ${this.imageNaturalHeight}
` : ''}
` : ''}
${this.src ? html` ${this.alt} ` : ''}
${this.loading && this.src ? html`
` : ''} ${this.error ? html`
${this.error}
` : ''}
`; } public zoomIn(): void { this.zoom = Math.min(10, this.zoom * 1.25); } public zoomOut(): void { this.zoom = Math.max(0.1, this.zoom / 1.25); if (this.zoom <= 1) { this.panX = 0; this.panY = 0; } } public resetZoom(): void { this.zoom = 1; this.panX = 0; this.panY = 0; } public fitToScreen(): void { this.zoom = 1; this.panX = 0; this.panY = 0; this.fit = 'contain'; } public actualSize(): void { this.zoom = 1; this.panX = 0; this.panY = 0; this.fit = 'actual'; } public download(): void { if (!this.src) return; const link = document.createElement('a'); link.href = this.src; link.download = this.src.split('/').pop() || 'image'; link.click(); } private handleImageLoad(e: Event): void { const img = e.target as HTMLImageElement; this.loading = false; this.error = ''; this.imageNaturalWidth = img.naturalWidth; this.imageNaturalHeight = img.naturalHeight; } private handleImageError(): void { this.loading = false; this.error = 'Failed to load image'; } private handleWheel(e: WheelEvent): void { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; const newZoom = Math.min(10, Math.max(0.1, this.zoom * delta)); this.zoom = newZoom; if (this.zoom <= 1) { this.panX = 0; this.panY = 0; } } private handleMouseDown(e: MouseEvent): void { if (this.zoom <= 1) return; this.isDragging = true; this.dragStartX = e.clientX; this.dragStartY = e.clientY; this.dragStartPanX = this.panX; this.dragStartPanY = this.panY; } private handleMouseMove(e: MouseEvent): void { if (!this.isDragging) return; this.panX = this.dragStartPanX + (e.clientX - this.dragStartX); this.panY = this.dragStartPanY + (e.clientY - this.dragStartY); } private handleMouseUp(): void { this.isDragging = false; } private handleDoubleClick(): void { if (this.zoom === 1) { this.zoom = 2; } else { this.zoom = 1; this.panX = 0; this.panY = 0; } } public updated(changedProperties: Map): void { super.updated(changedProperties); if (changedProperties.has('src')) { this.loading = true; this.error = ''; this.zoom = 1; this.panX = 0; this.panY = 0; this.imageNaturalWidth = 0; this.imageNaturalHeight = 0; } } }