Files
sovereign_browser/www/js/post-composer.mjs

866 lines
28 KiB
JavaScript

/* Inlined stubs for sovereign_browser — WebKit's custom sovereign://
* scheme does not resolve relative ES module imports, so we inline the
* dependencies instead of importing from separate .mjs files. These
* stubs are sufficient for the chat composer (no uploads, no preview). */
function uploadToAllServers() {
return Promise.reject(new Error('Blossom uploads are not available in sovereign_browser'));
}
function getBlobUrl(sha256, ext) {
return `blossom://${sha256}.${ext}`;
}
function htmlFormatText(text = '') {
const div = document.createElement('div');
div.textContent = String(text || '');
return div.innerHTML;
}
function hydrateNostrEntities() {
/* No-op in sovereign_browser. */
}
function debounce(fn, delay = 250) {
let timer = null;
return (...args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
function getFileExtension(file) {
const fromName = String(file?.name || '').split('.').pop();
if (fromName && fromName !== file?.name) return fromName.toLowerCase();
const fromType = String(file?.type || '').split('/').pop();
return fromType ? fromType.toLowerCase() : '';
}
function getCaretCharacterOffsetWithin(element) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return 0;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
}
function setCaretByCharacterOffset(element, offset) {
const selection = window.getSelection();
if (!selection) return;
const range = document.createRange();
let currentOffset = 0;
let found = false;
const walk = (node) => {
if (found) return;
if (node.nodeType === Node.TEXT_NODE) {
const textLength = node.textContent?.length || 0;
if (currentOffset + textLength >= offset) {
range.setStart(node, Math.max(0, offset - currentOffset));
range.collapse(true);
found = true;
} else {
currentOffset += textLength;
}
return;
}
for (const child of node.childNodes) {
walk(child);
if (found) return;
}
};
walk(element);
if (!found) {
range.selectNodeContents(element);
range.collapse(false);
}
selection.removeAllRanges();
selection.addRange(range);
}
function detectMentionQuery(text, caretOffset) {
const left = text.slice(0, caretOffset);
const match = left.match(/(^|\s)@([a-z0-9_.-]{1,32})$/i);
if (!match) return null;
const query = match[2] || '';
const start = left.length - query.length - 1;
const end = caretOffset;
return { query, start, end };
}
function escapeHtml(text = '') {
return String(text)
.replace(/&/g, '\u0026amp;')
.replace(/</g, '\u0026lt;')
.replace(/>/g, '\u0026gt;')
.replace(/"/g, '\u0026quot;')
.replace(/'/g, '\u0026#39;');
}
function toNpub(pubkeyOrNpub) {
if (!pubkeyOrNpub) return null;
if (String(pubkeyOrNpub).startsWith('npub1')) return String(pubkeyOrNpub);
try {
return window?.NostrTools?.nip19?.npubEncode?.(pubkeyOrNpub) || null;
} catch {
return null;
}
}
function positionDropdownNearCaret(hostEl, dropdownEl) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0).cloneRange();
range.collapse(true);
let rect = range.getBoundingClientRect();
if (!rect || (!rect.width && !rect.height)) {
const hostRect = hostEl.getBoundingClientRect();
rect = {
left: hostRect.left + 8,
top: hostRect.top + hostRect.height - 8,
bottom: hostRect.top + hostRect.height - 8
};
}
dropdownEl.style.left = `${Math.max(8, rect.left)}px`;
dropdownEl.style.top = `${Math.max(8, rect.bottom + 6)}px`;
}
function filterMentionCandidates(followedProfiles, query) {
const q = String(query || '').toLowerCase().trim();
if (!q) return [];
return followedProfiles
.filter((p) => {
const name = String(p?.name || '').toLowerCase();
const display = String(p?.display_name || '').toLowerCase();
const nip05 = String(p?.nip05 || '').toLowerCase();
const shortPk = String(p?.pubkey || '').slice(0, 12).toLowerCase();
return name.includes(q) || display.includes(q) || nip05.includes(q) || shortPk.includes(q);
})
.slice(0, 8);
}
function createUploadIcon(kind = 'upload') {
const normalizedKind = kind === 'image' ? 'image' : 'upload';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'post-composer-upload-icon';
if (normalizedKind === 'image') {
btn.classList.add('is-image-icon');
}
if (normalizedKind === 'image') {
btn.title = 'Attach image';
btn.setAttribute('aria-label', 'Attach image');
btn.innerHTML = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="2" ry="2"/>
<circle cx="9" cy="10" r="1.5"/>
<path d="M6 16l4-4 3 3 3-3 2 2"/>
</svg>
`;
return btn;
}
btn.title = 'Upload file';
btn.setAttribute('aria-label', 'Upload file');
btn.innerHTML = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<path d="M7 10l5-5 5 5"/>
<path d="M12 5v12"/>
</svg>
`;
return btn;
}
export function mountComposer(hostEl, options = {}) {
if (!hostEl) throw new Error('mountComposer requires a host element');
let followedProfiles = Array.isArray(options.followedProfiles) ? options.followedProfiles.slice() : [];
const showUploadIcon = options.showUploadIcon !== false;
const showPreview = options.showPreview !== false;
const autoHideOnSubmit = options.autoHideOnSubmit === true;
const submitOnEnter = options.submitOnEnter === true;
const hideOnEscape = options.hideOnEscape === true;
const alwaysShowSendButton = options.alwaysShowSendButton === true;
const clearBeforeSubmit = options.clearBeforeSubmit !== false;
const enableHistory = options.enableHistory !== false;
const historyKey = typeof options.historyKey === 'string' && options.historyKey.trim()
? options.historyKey.trim()
: 'post_composer_history_v1';
const historyMaxEntries = Math.max(1, Math.floor(Number(options.historyMaxEntries || 50)));
const layout = options.layout === 'inline' ? 'inline' : 'default';
const popupMode = options.popupMode === true;
const onSubmit = typeof options.onSubmit === 'function' ? options.onSubmit : null;
const onFileAttach = typeof options.onFileAttach === 'function' ? options.onFileAttach : null;
const uploadMode = options.uploadMode === 'direct-image' ? 'direct-image' : 'blossom-url';
const uploadIcon = options.uploadIcon === 'image' ? 'image' : (uploadMode === 'direct-image' ? 'image' : 'upload');
const defaultFileAccept = uploadMode === 'direct-image'
? 'image/*'
: 'image/*,video/*,audio/*,.pdf,.txt,.md,.zip';
const fileAccept = typeof options.fileAccept === 'string' && options.fileAccept.trim()
? options.fileAccept.trim()
: defaultFileAccept;
hostEl.setAttribute('contenteditable', 'true');
let wrapper = hostEl.parentElement;
if (!wrapper || !wrapper.classList.contains('post-composer-wrapper')) {
wrapper = document.createElement('div');
wrapper.className = 'post-composer-wrapper';
hostEl.parentNode?.insertBefore(wrapper, hostEl);
wrapper.appendChild(hostEl);
}
const editorShell = document.createElement('div');
editorShell.className = 'post-composer-editor-shell';
wrapper.insertBefore(editorShell, hostEl);
editorShell.appendChild(hostEl);
const dragOverlay = document.createElement('div');
dragOverlay.className = 'post-composer-drag-overlay';
dragOverlay.textContent = 'Drop files to upload';
editorShell.appendChild(dragOverlay);
const uploadStatus = document.createElement('div');
uploadStatus.className = 'post-composer-uploading';
uploadStatus.style.display = 'none';
editorShell.appendChild(uploadStatus);
let uploadButton = null;
if (showUploadIcon) {
uploadButton = createUploadIcon(uploadIcon);
editorShell.appendChild(uploadButton);
}
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.multiple = true;
fileInput.accept = fileAccept;
fileInput.style.display = 'none';
wrapper.appendChild(fileInput);
const previewEl = document.createElement('div');
previewEl.className = 'post-composer-preview';
previewEl.innerHTML = `
<div class="post-composer-preview-label">Preview</div>
<div class="post-composer-preview-content divPostContent"></div>
`;
const previewContentEl = previewEl.querySelector('.post-composer-preview-content');
const sendButton = document.createElement('button');
sendButton.type = 'button';
sendButton.className = 'post-composer-send-btn';
if (alwaysShowSendButton) sendButton.classList.add('is-persistent-visible');
sendButton.textContent = 'Send';
sendButton.disabled = true;
if (layout === 'inline') {
wrapper.classList.add('post-composer-wrapper--inline');
if (showPreview) wrapper.appendChild(previewEl);
const inlineRow = document.createElement('div');
inlineRow.className = 'post-composer-inline-row';
wrapper.appendChild(inlineRow);
inlineRow.appendChild(editorShell);
inlineRow.appendChild(sendButton);
} else if (popupMode) {
wrapper.classList.add('post-composer-wrapper--popup');
const scrollBody = document.createElement('div');
scrollBody.className = 'post-composer-popup-body';
scrollBody.appendChild(editorShell);
if (showPreview) scrollBody.appendChild(previewEl);
wrapper.appendChild(scrollBody);
const stickyFooter = document.createElement('div');
stickyFooter.className = 'post-composer-popup-footer';
stickyFooter.appendChild(sendButton);
wrapper.appendChild(stickyFooter);
} else {
if (showPreview) wrapper.appendChild(previewEl);
wrapper.appendChild(sendButton);
}
const mentionDropdown = document.createElement('div');
mentionDropdown.className = 'post-composer-mention-dropdown';
document.body.appendChild(mentionDropdown);
let dragDepth = 0;
let uploadingCount = 0;
let externallyDisabled = options.disabled === true;
let pendingAttachments = [];
let mentionState = {
query: '',
start: -1,
end: -1,
candidates: [],
activeIndex: 0,
visible: false
};
function loadHistory() {
if (!enableHistory) return [];
try {
const raw = localStorage.getItem(historyKey);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed)
? parsed.map((entry) => String(entry || '').trim()).filter(Boolean)
: [];
} catch {
return [];
}
}
function saveHistory(nextHistory) {
if (!enableHistory) return;
try {
localStorage.setItem(historyKey, JSON.stringify(nextHistory));
} catch {
// Ignore localStorage failures.
}
}
let history = loadHistory();
let historyIndex = -1;
let historyDraft = '';
function pushToHistory(text) {
if (!enableHistory) return;
const normalized = String(text || '').trim();
if (!normalized) return;
history = history.filter((item) => item !== normalized);
history.unshift(normalized);
if (history.length > historyMaxEntries) {
history = history.slice(0, historyMaxEntries);
}
saveHistory(history);
historyIndex = -1;
historyDraft = '';
}
function resetHistoryBrowseState() {
historyIndex = -1;
historyDraft = '';
}
function setEditorText(text = '') {
hostEl.innerText = String(text || '');
setCaretByCharacterOffset(hostEl, (hostEl.innerText || '').length);
renderPreview();
updateSendButtonVisibility();
}
function updateUploadingUi() {
if (uploadingCount > 0) {
uploadStatus.style.display = 'block';
uploadStatus.textContent = `Uploading ${uploadingCount} file${uploadingCount > 1 ? 's' : ''}`;
return;
}
if (pendingAttachments.length > 0) {
uploadStatus.style.display = 'block';
uploadStatus.textContent = `${pendingAttachments.length} image${pendingAttachments.length > 1 ? 's' : ''} attached`;
return;
}
uploadStatus.style.display = 'none';
uploadStatus.textContent = '';
}
function hideMentionDropdown() {
mentionState.visible = false;
mentionState.candidates = [];
mentionState.activeIndex = 0;
mentionDropdown.classList.remove('is-visible');
mentionDropdown.innerHTML = '';
}
function renderMentionDropdown() {
if (!mentionState.visible || mentionState.candidates.length === 0) {
hideMentionDropdown();
return;
}
mentionDropdown.innerHTML = mentionState.candidates
.map((profile, idx) => {
const display = profile.display_name || profile.name || `${String(profile.pubkey || '').slice(0, 8)}`;
const handle = profile.name ? `@${profile.name}` : `${String(profile.pubkey || '').slice(0, 12)}`;
const avatar = profile.picture
? `<img class="post-composer-mention-avatar" src="${escapeHtml(profile.picture)}" alt="" onerror="this.style.visibility='hidden'" />`
: `<div class="post-composer-mention-avatar" style="background:var(--muted-color);"></div>`;
return `
<div class="post-composer-mention-item ${idx === mentionState.activeIndex ? 'is-active' : ''}" data-index="${idx}">
${avatar}
<div class="post-composer-mention-meta">
<div class="post-composer-mention-name">${escapeHtml(display)}</div>
<div class="post-composer-mention-handle">${escapeHtml(handle)}</div>
</div>
</div>
`;
})
.join('');
mentionDropdown.classList.add('is-visible');
positionDropdownNearCaret(hostEl, mentionDropdown);
}
function replaceMentionWithProfile(profile) {
const npub = toNpub(profile?.npub || profile?.pubkey);
if (!npub) return;
const currentText = hostEl.innerText || '';
if (mentionState.start < 0 || mentionState.end < mentionState.start) return;
const replacement = `nostr:${npub}`;
const nextText = currentText.slice(0, mentionState.start) + replacement + currentText.slice(mentionState.end);
hostEl.innerText = nextText;
const nextCaret = mentionState.start + replacement.length;
setCaretByCharacterOffset(hostEl, nextCaret);
hideMentionDropdown();
queuePreviewRender();
updateSendButtonVisibility();
}
function updateMentionStateFromCaret() {
const text = hostEl.innerText || '';
const caret = getCaretCharacterOffsetWithin(hostEl);
const mention = detectMentionQuery(text, caret);
if (!mention) {
hideMentionDropdown();
return;
}
const candidates = filterMentionCandidates(followedProfiles, mention.query);
if (candidates.length === 0) {
hideMentionDropdown();
return;
}
mentionState = {
...mention,
candidates,
activeIndex: 0,
visible: true
};
renderMentionDropdown();
}
function normalizePendingAttachments(attachments) {
if (!Array.isArray(attachments)) return [];
return attachments
.map((att) => {
const dataUrl = String(att?.dataUrl || '').trim();
if (!dataUrl.startsWith('data:image/')) return null;
return {
id: String(att?.id || crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`),
name: String(att?.name || 'image').trim() || 'image',
mimeType: String(att?.mimeType || '').trim() || dataUrl.slice(5, dataUrl.indexOf(';') > 0 ? dataUrl.indexOf(';') : undefined),
dataUrl
};
})
.filter(Boolean);
}
async function insertUploadedFiles(files) {
const arr = Array.from(files || []).filter((f) => f && f.size > 0);
if (arr.length === 0) return;
uploadingCount += arr.length;
updateUploadingUi();
try {
if (uploadMode === 'direct-image') {
if (!onFileAttach) {
console.warn('[post-composer] uploadMode "direct-image" is enabled but onFileAttach is missing.');
return;
}
const attached = await onFileAttach(arr);
const normalized = normalizePendingAttachments(attached);
if (normalized.length > 0) {
pendingAttachments = [...pendingAttachments, ...normalized];
}
updateSendButtonVisibility();
return;
}
for (const file of arr) {
const { sha256 } = await uploadToAllServers(file);
const ext = getFileExtension(file);
const url = getBlobUrl(sha256, ext);
hostEl.focus();
const prefix = (hostEl.innerText || '').trim().length > 0 ? '\n' : '';
document.execCommand('insertText', false, `${prefix}${url}\n`);
}
} finally {
uploadingCount = Math.max(0, uploadingCount - arr.length);
updateUploadingUi();
queuePreviewRender();
}
}
function updateSendButtonVisibility() {
const hasContent = !!(hostEl.innerText || '').trim();
const hasAttachments = pendingAttachments.length > 0;
const canSubmit = hasContent || hasAttachments;
const interactive = canSubmit && uploadingCount === 0 && !externallyDisabled;
sendButton.classList.toggle('is-visible', canSubmit);
if (alwaysShowSendButton) sendButton.classList.add('is-visible');
sendButton.disabled = !interactive;
sendButton.tabIndex = (canSubmit || alwaysShowSendButton) ? 0 : -1;
}
function clearComposerContent() {
hostEl.innerHTML = '';
hostEl.innerText = '';
pendingAttachments = [];
renderPreview();
updateUploadingUi();
updateSendButtonVisibility();
hideMentionDropdown();
resetHistoryBrowseState();
}
function hideComposer() {
wrapper.style.display = 'none';
}
function showComposer() {
wrapper.style.display = '';
}
function renderPreview() {
if (!showPreview || !previewContentEl) return;
const raw = hostEl.innerText || '';
if (!raw.trim()) {
previewEl.classList.remove('is-visible');
previewContentEl.innerHTML = '';
return;
}
previewEl.classList.add('is-visible');
previewContentEl.innerHTML = htmlFormatText(raw);
hydrateNostrEntities(previewContentEl);
}
const queuePreviewRender = debounce(renderPreview, 250);
function onInput() {
queuePreviewRender();
updateMentionStateFromCaret();
updateSendButtonVisibility();
if (historyIndex !== -1) {
resetHistoryBrowseState();
}
}
function onKeydown(event) {
if (event.key === 'Tab' && !event.shiftKey) {
const hasContent = !!(hostEl.innerText || '').trim();
if (hasContent && !mentionState.visible) {
event.preventDefault();
sendButton.focus();
return;
}
}
const isEnterKey = event.key === 'Enter' || event.code === 'Enter' || event.code === 'NumpadEnter';
if (isEnterKey && (event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && !mentionState.visible) {
event.preventDefault();
event.stopPropagation();
onSendClick();
return;
}
if (submitOnEnter && event.key === 'Enter' && !event.shiftKey && !mentionState.visible) {
event.preventDefault();
onSendClick();
return;
}
if (event.key === 'Escape') {
if (mentionState.visible) {
event.preventDefault();
hideMentionDropdown();
return;
}
if (hideOnEscape) {
event.preventDefault();
hideComposer();
return;
}
}
if (!mentionState.visible && enableHistory) {
const currentText = hostEl.innerText || '';
const caretOffset = getCaretCharacterOffsetWithin(hostEl);
const singleLine = !currentText.includes('\n');
if (event.key === 'ArrowUp') {
const atStart = caretOffset <= 0;
if (atStart || singleLine) {
event.preventDefault();
if (historyIndex === -1) {
historyDraft = currentText;
}
if (historyIndex < history.length - 1) {
historyIndex += 1;
setEditorText(history[historyIndex] || '');
}
return;
}
}
if (event.key === 'ArrowDown') {
const atEnd = caretOffset >= currentText.length;
if ((atEnd || singleLine) && historyIndex >= 0) {
event.preventDefault();
historyIndex -= 1;
if (historyIndex < 0) {
setEditorText(historyDraft || '');
} else {
setEditorText(history[historyIndex] || '');
}
return;
}
}
}
if (!mentionState.visible) return;
if (event.key === 'ArrowDown') {
event.preventDefault();
mentionState.activeIndex = (mentionState.activeIndex + 1) % mentionState.candidates.length;
renderMentionDropdown();
return;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
mentionState.activeIndex = (mentionState.activeIndex - 1 + mentionState.candidates.length) % mentionState.candidates.length;
renderMentionDropdown();
return;
}
if (event.key === 'Enter' || event.key === 'Tab') {
event.preventDefault();
const selected = mentionState.candidates[mentionState.activeIndex];
if (selected) replaceMentionWithProfile(selected);
return;
}
}
function onSelectionChange() {
if (!mentionState.visible) return;
updateMentionStateFromCaret();
}
function onDocumentClick(event) {
if (wrapper.contains(event.target) || mentionDropdown.contains(event.target)) return;
hideMentionDropdown();
}
function onDragEnter(event) {
if (!event.dataTransfer?.types?.includes('Files')) return;
event.preventDefault();
dragDepth += 1;
editorShell.classList.add('is-dragover');
}
function onDragOver(event) {
if (!event.dataTransfer?.types?.includes('Files')) return;
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
}
function onDragLeave(event) {
if (!event.dataTransfer?.types?.includes('Files')) return;
event.preventDefault();
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) editorShell.classList.remove('is-dragover');
}
async function onDrop(event) {
if (!event.dataTransfer?.files?.length) return;
event.preventDefault();
dragDepth = 0;
editorShell.classList.remove('is-dragover');
await insertUploadedFiles(event.dataTransfer.files);
}
async function onFileInputChange(event) {
const files = event.target?.files;
if (!files || files.length === 0) return;
await insertUploadedFiles(files);
fileInput.value = '';
}
async function onPaste(event) {
const clipboardItems = Array.from(event.clipboardData?.items || []);
const imageFiles = clipboardItems
.filter((item) => item.kind === 'file' && String(item.type || '').startsWith('image/'))
.map((item) => item.getAsFile())
.filter(Boolean);
if (imageFiles.length === 0) return;
event.preventDefault();
await insertUploadedFiles(imageFiles);
}
function onMentionDropdownMouseDown(event) {
const item = event.target.closest('.post-composer-mention-item');
if (!item) return;
event.preventDefault();
const index = Number(item.dataset.index);
if (Number.isNaN(index)) return;
const selected = mentionState.candidates[index];
if (selected) replaceMentionWithProfile(selected);
}
async function onSendClick() {
if (sendButton.disabled) return;
if (!onSubmit) return;
const text = (hostEl.innerText || '').trim();
const attachments = pendingAttachments.slice();
if (!text && attachments.length === 0) return;
const previousText = text;
const previousAttachments = attachments;
if (clearBeforeSubmit) {
clearComposerContent();
if (!autoHideOnSubmit) {
hostEl.focus();
setCaretByCharacterOffset(hostEl, 0);
}
}
try {
const submitted = await onSubmit(previousText, { attachments: previousAttachments });
if (submitted === false) {
if (clearBeforeSubmit) {
setEditorText(previousText);
pendingAttachments = previousAttachments;
updateUploadingUi();
updateSendButtonVisibility();
}
return;
}
if (previousText) {
pushToHistory(previousText);
}
if (autoHideOnSubmit) {
if (!clearBeforeSubmit) {
clearComposerContent();
}
hideComposer();
} else if (!clearBeforeSubmit) {
pendingAttachments = [];
queuePreviewRender();
updateUploadingUi();
updateSendButtonVisibility();
hostEl.focus();
setCaretByCharacterOffset(hostEl, 0);
}
} catch (error) {
if (clearBeforeSubmit) {
setEditorText(previousText);
pendingAttachments = previousAttachments;
updateUploadingUi();
updateSendButtonVisibility();
hostEl.focus();
setCaretByCharacterOffset(hostEl, (hostEl.innerText || '').length);
}
console.error('[post-composer] Submit failed:', error);
}
}
hostEl.addEventListener('input', onInput);
hostEl.addEventListener('keydown', onKeydown);
hostEl.addEventListener('dragenter', onDragEnter);
hostEl.addEventListener('dragover', onDragOver);
hostEl.addEventListener('dragleave', onDragLeave);
hostEl.addEventListener('drop', onDrop);
document.addEventListener('selectionchange', onSelectionChange);
document.addEventListener('mousedown', onDocumentClick);
hostEl.addEventListener('paste', onPaste);
if (uploadButton) {
uploadButton.addEventListener('click', () => fileInput.click());
}
fileInput.addEventListener('change', onFileInputChange);
mentionDropdown.addEventListener('mousedown', onMentionDropdownMouseDown);
sendButton.addEventListener('click', onSendClick);
queuePreviewRender();
updateSendButtonVisibility();
return {
refresh(nextOptions = {}) {
if (Array.isArray(nextOptions.followedProfiles)) {
followedProfiles = nextOptions.followedProfiles.slice();
}
queuePreviewRender();
},
clear() {
clearComposerContent();
},
setDisabled(disabled) {
externallyDisabled = !!disabled;
updateSendButtonVisibility();
},
show() {
showComposer();
},
hide() {
hideComposer();
},
isUploading() {
return uploadingCount > 0;
},
destroy() {
hostEl.removeEventListener('input', onInput);
hostEl.removeEventListener('keydown', onKeydown);
hostEl.removeEventListener('dragenter', onDragEnter);
hostEl.removeEventListener('dragover', onDragOver);
hostEl.removeEventListener('dragleave', onDragLeave);
hostEl.removeEventListener('drop', onDrop);
document.removeEventListener('selectionchange', onSelectionChange);
document.removeEventListener('mousedown', onDocumentClick);
hostEl.removeEventListener('paste', onPaste);
fileInput.removeEventListener('change', onFileInputChange);
mentionDropdown.removeEventListener('mousedown', onMentionDropdownMouseDown);
sendButton.removeEventListener('click', onSendClick);
if (mentionDropdown.parentNode) mentionDropdown.parentNode.removeChild(mentionDropdown);
if (uploadButton?.parentNode) uploadButton.parentNode.removeChild(uploadButton);
if (dragOverlay.parentNode) dragOverlay.parentNode.removeChild(dragOverlay);
if (uploadStatus.parentNode) uploadStatus.parentNode.removeChild(uploadStatus);
if (previewEl.parentNode) previewEl.parentNode.removeChild(previewEl);
if (sendButton.parentNode) sendButton.parentNode.removeChild(sendButton);
if (editorShell.parentNode) editorShell.parentNode.removeChild(editorShell);
if (fileInput.parentNode) fileInput.parentNode.removeChild(fileInput);
}
};
}
export default mountComposer;