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.src ? html`

` : ''}
${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;
}
}
}