import { Component, Prop, Event, EventEmitter, State, Method, h } from '@stencil/core'; interface ImagePreview { id: string; file: File; dataUrl: string; } @Component({ tag: 'bcx-message-composer', styleUrl: 'bcx-message-composer.scss', shadow: true, }) export class BcxMessageComposer { @Prop() disabled: boolean = false; @Prop() loading: boolean = false; @Prop() placeholder: string = 'Type your message...'; @Prop() maxLength: number = 1000; @Prop() theme: 'light' | 'dark' = 'light'; @Prop() isAttachmentsDisabled: boolean = false; @State() message: string = ''; @State() images: ImagePreview[] = []; @Event() messageSubmit: EventEmitter<{ content: string; images: File[] }>; @Event() attachmentsChange: EventEmitter<{ count: number }>; private textareaRef: HTMLTextAreaElement; private fileInputRef: HTMLInputElement; private emitAttachmentsCount() { this.attachmentsChange.emit({ count: this.images.length }); } private handleInput = (event: Event) => { const target = event.target as HTMLTextAreaElement; this.message = target.value; this.adjustTextareaHeight(); }; private handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.submitMessage(); } }; private handleSubmit = (event: Event) => { event.preventDefault(); this.submitMessage(); }; @Method() async clearAttachments(): Promise { this.images = []; this.emitAttachmentsCount(); } private submitMessage() { const trimmedMessage = this.message.trim(); if (!this.disabled && trimmedMessage) { this.messageSubmit.emit({ content: trimmedMessage, images: this.images.map(img => img.file), }); this.message = ''; this.images = []; this.emitAttachmentsCount(); this.adjustTextareaHeight(); } } private handleImageUpload = (event: Event) => { if (this.isAttachmentsDisabled) { return; } const target = event.target as HTMLInputElement; const files = target.files; if (files && files.length > 0) { const maxImages = 3; const remainingSlots = maxImages - this.images.length; for (let i = 0; i < Math.min(files.length, remainingSlots); i++) { const file = files[i]; const allowedFormats = ['png', 'jpg', 'jpeg', 'gif', 'webp']; const fileExtension = file.name.split('.').pop()?.toLowerCase(); const mimeType = file.type.toLowerCase(); const isValidFormat = allowedFormats.some(format => fileExtension === format || mimeType === `image/${format}`); if (!isValidFormat) { console.warn(`File ${file.name} is not a supported image format. Allowed: ${allowedFormats.join(', ')}`); continue; } if (file.size > 5 * 1024 * 1024) { console.warn(`File ${file.name} is too large (max 5MB)`); continue; } const reader = new FileReader(); reader.onload = e => { const dataUrl = e.target?.result as string; const imagePreview: ImagePreview = { id: Math.random().toString(36).substr(2, 9), file, dataUrl, }; this.images = [...this.images, imagePreview]; // Notify parent after Stencil applies image state (avoids parent re-render racing preview paint). queueMicrotask(() => this.emitAttachmentsCount()); }; reader.onerror = () => { console.warn('[bcx-message-composer] Failed to read image file for preview'); }; reader.readAsDataURL(file); } } target.value = ''; }; private removeImage = (imageId: string) => { this.images = this.images.filter(img => img.id !== imageId); this.emitAttachmentsCount(); }; private triggerImageUpload = () => { if (this.disabled || this.loading || this.images.length >= 3 || this.isAttachmentsDisabled) { return; } this.fileInputRef?.click(); }; private adjustTextareaHeight() { if (this.textareaRef) { this.textareaRef.style.height = 'auto'; this.textareaRef.style.height = Math.min(this.textareaRef.scrollHeight, 160) + 'px'; } } render() { const isSubmitDisabled = !this.message.trim() || this.disabled || this.loading; const canUploadMoreImages = !this.disabled && !this.loading && this.images.length < 3 && !this.isAttachmentsDisabled; const actionsClass = this.isAttachmentsDisabled ? 'bcx-composer__actions bcx-composer__actions--no-attachments' : 'bcx-composer__actions'; return (
{!this.isAttachmentsDisabled && ( (this.fileInputRef = el)} type="file" accept="image/png,image/jpg,image/jpeg,image/gif,image/webp" multiple onChange={this.handleImageUpload} style={{ display: 'none' }} data-adblock-bypass="true" /> )} {this.images.length > 0 && (
{this.images.map(image => (
Preview
))}
)}