import { html, css, TemplateResult, CSSResultArray } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import NileElement from '../../internal/nile-element'; import '../../nile-input/index'; import '../../nile-button/index'; /** * Floating image insert/edit panel, matching nile-rich-text-editor's image * dialog: URL + alt + caption (nile-input) and an Insert action. When * `upload-from-computer` is set it also offers an "or / Upload from computer" * button (with max-size validation), matching the RTE's `uploadfromcomputer` * flag. An edit mode pre-fills the fields and adds Remove. * * The host positions it and listens for: * - `wysiwyg-image-apply` detail: { src, alt, caption } (insert / edit) * - `wysiwyg-image-file` detail: { file, alt, caption } (upload) * - `wysiwyg-image-remove` (delete active image) * - `wysiwyg-image-cancel` (Escape) */ @customElement('nile-wysiwyg-image-panel') export class NileWysiwygImagePanel extends NileElement { @property({ type: Boolean, reflect: true }) open = false; /** True when editing an already-inserted image (shows Remove, "Save"). */ @property({ type: Boolean }) editing = false; /** Shows the "Upload from computer" button below the URL field. */ @property({ type: Boolean, attribute: 'upload-from-computer' }) uploadFromComputer = false; /** Maximum allowed upload size in bytes (default 2 MB). */ @property({ type: Number, attribute: 'max-file-size' }) maxFileSize = 2 * 1024 * 1024; @state() private srcValue = ''; @state() private altValue = ''; @state() private captionValue = ''; @state() private uploadedFileName = ''; @state() private errorMessage = ''; /** File staged by "Upload from computer", applied on Insert. */ private stagedFile: File | null = null; public static get styles(): CSSResultArray { return [ css` :host { font-family: var( --nile-font-family-sans-serif, var(--ng-font-family-body, system-ui, -apple-system, sans-serif) ); position: absolute; z-index: 20; display: none; } :host([open]) { display: block; } .image-panel { display: flex; flex-direction: column; gap: 12px; min-width: 340px; max-width: 420px; padding: 12px; background: var(--nile-wysiwyg-popover-bg, #ffffff); border: 1px solid var(--nile-colors-neutral-400, #d0d5dd); border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); font-size: 13px; } nile-input { width: 100%; } .divider { display: flex; align-items: center; gap: 8px; color: var(--nile-colors-neutral-500, #98a2b3); font-size: 12px; } .divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: var(--nile-colors-neutral-300, #e4e7ec); } .upload-row { display: flex; flex-direction: column; align-items: stretch; gap: 6px; } .upload-btn { width: 100%; } /* Center the icon + label within the full-width upload button. */ .upload-btn::part(base) { width: 100%; justify-content: center; } .upload-filename { color: var(--nile-colors-neutral-600, #475467); font-size: 12px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .hidden-file { display: none; } .error { color: var(--nile-colors-danger-600, #d92d20); font-size: 12px; } .actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; width: 100%; } .actions .spacer { flex: 1; } `, ]; } public focusInput(): void { this.updateComplete.then(() => { const input = this.shadowRoot?.querySelector('nile-input') as HTMLElement | null; input?.focus(); }); } /** Clears every field and returns to insert (non-editing) mode. */ public reset(): void { this.editing = false; this.srcValue = ''; this.altValue = ''; this.captionValue = ''; this.uploadedFileName = ''; this.errorMessage = ''; this.stagedFile = null; } /** Pre-fills the panel for editing an existing image. */ public prefill(data: { src?: string; alt?: string; caption?: string }): void { this.editing = true; this.srcValue = data.src ?? ''; this.altValue = data.alt ?? ''; this.captionValue = data.caption ?? ''; this.uploadedFileName = ''; this.errorMessage = ''; this.stagedFile = null; } private onSrc(event: CustomEvent): void { this.srcValue = (event.detail?.value ?? '') as string; if (this.srcValue && this.stagedFile) { this.stagedFile = null; this.uploadedFileName = ''; } this.errorMessage = ''; } private onAlt(event: CustomEvent): void { this.altValue = (event.detail?.value ?? '') as string; } private onCaption(event: CustomEvent): void { this.captionValue = (event.detail?.value ?? '') as string; } private triggerFilePicker(): void { const input = this.shadowRoot?.querySelector('.hidden-file') as HTMLInputElement | null; input?.click(); } private onFileChange(event: Event): void { const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { this.errorMessage = 'Please choose an image file.'; input.value = ''; return; } if (file.size > this.maxFileSize) { const mb = Math.round((this.maxFileSize / (1024 * 1024)) * 10) / 10; this.errorMessage = `Image must be under ${mb} MB.`; input.value = ''; return; } this.stagedFile = file; this.uploadedFileName = file.name; this.srcValue = ''; this.errorMessage = ''; input.value = ''; } private handleSubmit(): void { const alt = this.altValue.trim(); const caption = this.captionValue.trim(); if (this.stagedFile) { this.emit('wysiwyg-image-file', { file: this.stagedFile, alt, caption }); return; } const src = this.srcValue.trim(); if (!src) { this.errorMessage = this.uploadFromComputer ? 'Add an image URL or upload a file.' : 'Add an image URL.'; return; } this.emit('wysiwyg-image-apply', { src, alt, caption }); } private handleKeydown(event: KeyboardEvent): void { if (event.key === 'Escape') { event.stopPropagation(); this.emit('wysiwyg-image-cancel'); } else if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); this.handleSubmit(); } } public render(): TemplateResult { return html`