diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 87f7b79..9e363a4 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -136,52 +136,58 @@ } } - // In-site reply (Markdown-ish: textarea + toolbar; server renders sanitized HTML). + // In-site reply: small admin-only WYSIWYG (contenteditable + execCommand). + // The editor's HTML is sanitized SERVER-SIDE; the client just collects it. let replyingId = $state(null); - let replyText = $state(''); let replyBusy = $state(false); let replyErr = $state(''); - let replyTextarea = $state(null); - function openReply(f) { replyingId = f.id; replyText = ''; replyErr = ''; } - function cancelReply() { replyingId = null; replyText = ''; replyErr = ''; } + let replyEmpty = $state(true); + let replyEditor = $state(null); + const SIZE_PX = { small: 13, normal: 15, large: 18, xlarge: 22 }; - // Wrap the current selection (e.g. **bold**), restoring focus + selection. - function mdWrap(token) { - const el = replyTextarea; - if (!el) return; - const s = el.selectionStart, e = el.selectionEnd; - const sel = replyText.slice(s, e) || 'text'; - replyText = replyText.slice(0, s) + token + sel + token + replyText.slice(e); - queueMicrotask(() => { - el.focus(); - el.selectionStart = s + token.length; - el.selectionEnd = s + token.length + sel.length; - }); + function openReply(f) { replyingId = f.id; replyErr = ''; replyEmpty = true; } + function cancelReply() { replyingId = null; replyErr = ''; } + function focusEditor(node) { queueMicrotask(() => node.focus()); } + function onEditorInput() { replyEmpty = !(replyEditor?.textContent || '').trim(); } + // Paste as plain text — never let rich/pasted HTML into the editor. + function onPaste(e) { + e.preventDefault(); + const text = (e.clipboardData || window.clipboardData).getData('text/plain'); + document.execCommand('insertText', false, text); } - // Prefix each selected line (e.g. "- " or "## "). - function mdPrefix(prefix) { - const el = replyTextarea; - if (!el) return; - const s = el.selectionStart, e = el.selectionEnd; - const start = replyText.lastIndexOf('\n', s - 1) + 1; - let end = replyText.indexOf('\n', e); - if (end === -1) end = replyText.length; - const block = replyText.slice(start, end) || 'item'; - const prefixed = block.split('\n').map((ln) => prefix + ln).join('\n'); - replyText = replyText.slice(0, start) + prefixed + replyText.slice(end); - queueMicrotask(() => el.focus()); + // Bold/italic/lists via execCommand; styleWithCSS=false → semantic //