import { customElement, html, property, css, cssManager, state, type TemplateResult, } from '@design.estate/dees-element'; import { DeesInputBase } from '../dees-input-base.js'; import '../dees-icon.js'; import '../dees-label.js'; import { ProfilePictureModal } from './profilepicture.modal.js'; import { demoFunc } from './dees-input-profilepicture.demo.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-profilepicture': DeesInputProfilePicture; } } export type ProfileShape = 'square' | 'round'; @customElement('dees-input-profilepicture') export class DeesInputProfilePicture extends DeesInputBase { public static demo = demoFunc; @property({ type: String }) public value: string = ''; // Base64 encoded image or URL @property({ type: String }) public shape: ProfileShape = 'round'; @property({ type: Number }) public size: number = 120; @property({ type: String }) public placeholder: string = ''; @property({ type: Boolean }) public allowUpload: boolean = true; @property({ type: Boolean }) public allowDelete: boolean = true; @property({ type: Number }) public maxFileSize: number = 5 * 1024 * 1024; // 5MB @property({ type: Array }) public acceptedFormats: string[] = ['image/jpeg', 'image/png', 'image/webp']; @property({ type: Number }) public outputSize: number = 800; // Output resolution in pixels @property({ type: Number }) public outputQuality: number = 0.95; // 0-1 quality for JPEG @state() private isHovered: boolean = false; @state() private isDragging: boolean = false; @state() private isLoading: boolean = false; private modalInstance: ProfilePictureModal | null = null; public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` :host { display: block; position: relative; } .input-wrapper { display: flex; flex-direction: column; gap: 16px; } .profile-container { position: relative; display: inline-block; cursor: pointer; transition: all 0.3s ease; } .profile-container:hover { transform: scale(1.02); } .profile-picture { width: var(--size, 120px); height: var(--size, 120px); background: ${cssManager.bdTheme('#f5f5f5', '#18181b')}; border: 3px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; transition: all 0.3s ease; } .profile-picture.round { border-radius: 50%; } .profile-picture.square { border-radius: 12px; } .profile-picture.dragging { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; box-shadow: 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.15)', 'rgba(96, 165, 250, 0.15)')}; } .profile-picture:hover { border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; } .profile-picture:disabled { cursor: not-allowed; opacity: 0.5; } .profile-image { width: 100%; height: 100%; object-fit: cover; } .placeholder-icon { color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; } .overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; } .profile-container:hover .overlay { opacity: 1; } .overlay-content { display: flex; gap: 12px; } .overlay-button { width: 40px; height: 40px; border-radius: 50%; background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.95)', 'rgba(39, 39, 42, 0.95)')}; border: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')}; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; pointer-events: auto; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .overlay-button:hover { background: ${cssManager.bdTheme('#ffffff', '#3f3f46')}; transform: scale(1.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } .overlay-button.delete { background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.9)', 'rgba(220, 38, 38, 0.9)')}; color: white; border-color: transparent; } .overlay-button.delete:hover { background: ${cssManager.bdTheme('#ef4444', '#dc2626')}; } .drop-zone-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: white; font-weight: 500; pointer-events: none; } .hidden-input { display: none; } /* Loading animation */ .loading-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.8)', 'rgba(0, 0, 0, 0.8)')}; display: flex; align-items: center; justify-content: center; border-radius: inherit; opacity: 0; pointer-events: none; transition: opacity 0.2s ease; } .loading-overlay.show { opacity: 1; pointer-events: auto; } .loading-spinner { width: 40px; height: 40px; border: 3px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; border-radius: 50%; animation: spin 0.6s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } @keyframes pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.8; } 100% { transform: scale(1); opacity: 1; } } .profile-picture.clicking { animation: pulse 0.3s ease-out; } `, ]; render(): TemplateResult { return html` ${this.value ? html` ` : html` `} ${this.isDragging ? html` Drop image here ` : ''} ${this.value && !this.disabled ? html` ${this.allowUpload ? html` { e.stopPropagation(); this.openModal(); }} title="Change picture"> ` : ''} ${this.allowDelete ? html` { e.stopPropagation(); this.deletePicture(); }} title="Delete picture"> ` : ''} ` : ''} ${this.isLoading && !this.value ? html` ` : ''} `; } private handleClick(): void { if (this.disabled || !this.allowUpload) return; if (!this.value) { // If no image, open file picker this.isLoading = true; const input = this.shadowRoot!.querySelector('.hidden-input') as HTMLInputElement; // Set up a focus handler to detect when the dialog is closed without selection const handleFocus = () => { setTimeout(() => { // Check if no file was selected if (!input.files || input.files.length === 0) { this.isLoading = false; } window.removeEventListener('focus', handleFocus); }, 300); }; window.addEventListener('focus', handleFocus); input.click(); } } private handleFileSelect(event: Event): void { const input = event.target as HTMLInputElement; const file = input.files?.[0]; // Always reset loading state when file dialog interaction completes this.isLoading = false; if (file) { this.processFile(file); } // Reset input to allow selecting the same file again input.value = ''; } private handleDragOver(event: DragEvent): void { event.preventDefault(); if (!this.disabled && this.allowUpload) { this.isDragging = true; } } private handleDragLeave(): void { this.isDragging = false; } private handleDrop(event: DragEvent): void { event.preventDefault(); this.isDragging = false; if (this.disabled || !this.allowUpload) return; const file = event.dataTransfer?.files[0]; if (file) { this.processFile(file); } } private async processFile(file: File): Promise { // Validate file type if (!this.acceptedFormats.includes(file.type)) { console.error('Invalid file type:', file.type); return; } // Validate file size if (file.size > this.maxFileSize) { console.error('File too large:', file.size); return; } // Read file as base64 const reader = new FileReader(); reader.onload = async (e) => { const base64 = e.target?.result as string; // Open modal for cropping await this.openModal(base64); }; reader.readAsDataURL(file); } private async openModal(initialImage?: string): Promise { const imageToEdit = initialImage || this.value; if (!imageToEdit) { // If no image provided, open file picker const input = this.shadowRoot!.querySelector('.hidden-input') as HTMLInputElement; input.click(); return; } // Create and show modal this.modalInstance = new ProfilePictureModal(); this.modalInstance.shape = this.shape; this.modalInstance.initialImage = imageToEdit; this.modalInstance.outputSize = this.outputSize; this.modalInstance.outputQuality = this.outputQuality; this.modalInstance.addEventListener('save', (event: CustomEvent) => { this.value = event.detail.croppedImage; this.changeSubject.next(this); }); document.body.appendChild(this.modalInstance); } private deletePicture(): void { this.value = ''; this.changeSubject.next(this); } public getValue(): string { return this.value; } public setValue(value: string): void { this.value = value; } }