diff --git a/public/components/markdown_editor.js b/public/components/markdown_editor.js index a71cdaa..923f115 100644 --- a/public/components/markdown_editor.js +++ b/public/components/markdown_editor.js @@ -1,8 +1,8 @@ -// Split-pane Markdown editor. -// - Left: textarea (raw markdown) -// - Right: rendered HTML preview via marked → DOMPurify -// Save button calls a caller-supplied save(value) that returns a promise -// resolving to the updated row. We surface the last edited_at timestamp. +// Single-pane Markdown view with an Edit/Preview toggle. +// - Default: rendered HTML (marked → DOMPurify). +// - Click "Edit" to replace the pane in place with a raw-markdown textarea + Save. +// - Click "Preview" to re-render and go back to read mode. +// save(value) is caller-supplied and returns a promise resolving to the updated row. import { marked } from '../vendor/marked.esm.js'; import DOMPurify from '../vendor/purify.es.mjs'; @@ -11,6 +11,8 @@ import { el } from '../dom.js'; marked.setOptions({ gfm: true, breaks: true }); export function markdownEditor({ initial = '', save }) { + let editing = false; + const ta = el('textarea', { style: { width: '100%', minHeight: '420px', resize: 'vertical', @@ -21,36 +23,54 @@ export function markdownEditor({ initial = '', save }) { const preview = el('div', { class: 'md-preview' }); function rerender() { - const html = DOMPurify.sanitize(marked.parse(ta.value)); - preview.innerHTML = html; // sanitized — see dom.js note about html: opt-in + preview.innerHTML = DOMPurify.sanitize(marked.parse(ta.value)); // sanitized } - ta.addEventListener('input', rerender); rerender(); + // The pane shows EITHER the preview (read) or the textarea (edit). + const pane = el('div', {}, preview); + const stamp = el('span', { class: 'muted', style: { fontSize: '11px' } }, ''); - const btn = el('button', { + + const saveBtn = el('button', { class: 'primary', + style: { display: 'none' }, onclick: async () => { - btn.disabled = true; + saveBtn.disabled = true; try { const updated = await save(ta.value); - if (updated?.updated_at) stamp.textContent = 'Saved ' + new Date(updated.updated_at).toLocaleString(); - else stamp.textContent = 'Saved'; + stamp.textContent = updated?.updated_at + ? 'Saved ' + new Date(updated.updated_at).toLocaleString() + : 'Saved'; } catch (e) { stamp.textContent = 'Save failed: ' + e.message; } finally { - btn.disabled = false; + saveBtn.disabled = false; } } }, 'Save'); + const toggle = el('button', { + onclick: () => { + editing = !editing; + if (editing) { + pane.replaceChild(ta, preview); + ta.focus(); + toggle.textContent = 'Preview'; + saveBtn.style.display = ''; + } else { + rerender(); + pane.replaceChild(preview, ta); + toggle.textContent = 'Edit'; + saveBtn.style.display = 'none'; + } + } + }, 'Edit'); + return el('div', {}, - el('div', { class: 'row' }, - el('div', { class: 'card' }, el('h3', {}, 'Edit'), ta), - el('div', { class: 'card' }, el('h3', {}, 'Preview'), preview) + el('div', { style: { display: 'flex', gap: '12px', alignItems: 'center', marginBottom: '8px' } }, + toggle, saveBtn, stamp ), - el('div', { style: { display: 'flex', gap: '12px', alignItems: 'center', marginTop: '8px' } }, - btn, stamp - ) + el('div', { class: 'card' }, pane) ); }