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']; @state() private isHovered: boolean = false; @state() private isDragging: 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('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; border: 3px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; 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('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; box-shadow: 0 0 0 4px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')}; } .profile-picture:hover { border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; } .profile-picture:disabled { cursor: not-allowed; opacity: 0.5; } .profile-image { width: 100%; height: 100%; object-fit: cover; } .placeholder-icon { color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .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: rgba(255, 255, 255, 0.9); border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; pointer-events: auto; } .overlay-button:hover { background: white; transform: scale(1.1); } .overlay-button.delete { background: rgba(239, 68, 68, 0.9); color: white; } .overlay-button.delete:hover { background: rgb(239, 68, 68); } .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; } `, ]; render(): TemplateResult { return html`
${this.value ? html` Profile picture ` : html` `} ${this.isDragging ? html`
Drop image here
` : ''} ${this.value && !this.disabled ? html`
${this.allowUpload ? html` ` : ''} ${this.allowDelete ? html` ` : ''}
` : ''}
`; } private handleClick(): void { if (this.disabled || !this.allowUpload) return; if (!this.value) { // If no image, open file picker const input = this.shadowRoot!.querySelector('.hidden-input') as HTMLInputElement; input.click(); } } private handleFileSelect(event: Event): void { const input = event.target as HTMLInputElement; const file = input.files?.[0]; 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.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; } }