Files
dees-catalog/ts_web/elements/profilepicture/dees-input-profilepicture.ts

361 lines
9.3 KiB
TypeScript
Raw Normal View History

2025-06-30 11:35:38 +00:00
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<DeesInputProfilePicture> {
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`
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
<div
class="profile-container"
@click=${this.handleClick}
@dragover=${this.handleDragOver}
@dragleave=${this.handleDragLeave}
@drop=${this.handleDrop}
style="--size: ${this.size}px"
>
<div class="profile-picture ${this.shape} ${this.isDragging ? 'dragging' : ''}">
${this.value ? html`
<img class="profile-image" src="${this.value}" alt="Profile picture" />
` : html`
<dees-icon class="placeholder-icon" icon="lucide:user" iconSize="${this.size * 0.5}"></dees-icon>
`}
${this.isDragging ? html`
<div class="overlay" style="opacity: 1">
<div class="drop-zone-text">
Drop image here
</div>
</div>
` : ''}
${this.value && !this.disabled ? html`
<div class="overlay">
<div class="overlay-content">
${this.allowUpload ? html`
<button class="overlay-button" @click=${(e: Event) => { e.stopPropagation(); this.openModal(); }} title="Change picture">
<dees-icon icon="lucide:pencil" iconSize="20"></dees-icon>
</button>
` : ''}
${this.allowDelete ? html`
<button class="overlay-button delete" @click=${(e: Event) => { e.stopPropagation(); this.deletePicture(); }} title="Delete picture">
<dees-icon icon="lucide:trash2" iconSize="20"></dees-icon>
</button>
` : ''}
</div>
</div>
` : ''}
</div>
</div>
<input
type="file"
class="hidden-input"
accept="${this.acceptedFormats.join(',')}"
@change=${this.handleFileSelect}
/>
</div>
`;
}
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<void> {
// 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<void> {
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;
}
}