/** * Binds the described markdown field's Write/Preview chrome. * * `crate::quasi::rich_field` emits the wrapper, the two mode buttons and an * empty preview pane; makeover-webview's rules keep all of it hidden until * something sets `data-ready`, so a reader with no script gets the plain * textarea and nothing that looks live and answers nothing. This is what sets * it. * * The pane is filled from /api/preview/markdown, which runs the same pipeline * that publishes (docengine over ammonia, plus media-host restriction). That is * why `innerHTML` is safe here and why the preview matches what saving does; * see src/routes/api/preview.rs. * * Runs once per wrapper. Rebinds after an HTMX swap via `htmx:after:settle`, * which fires on the swap target rather than the source (CONTRIBUTING.md, * "HTMX partial re-initialization"). */ (function () { 'use strict'; var ENDPOINT = '/api/preview/markdown'; function setMode(wrapper, mode) { wrapper.dataset.mode = mode; wrapper.querySelectorAll('[data-editor-mode]').forEach(function (button) { var chosen = button.dataset.editorMode === mode; button.classList.toggle('chosen', chosen); button.setAttribute('aria-pressed', String(chosen)); }); } function renderPreview(area, pane, state) { var source = area.value; // Nothing changed since the last look, so the pane already holds the // answer. Also covers flipping back and forth while reading. if (state.rendered === source) { return; } if (!source.trim()) { state.rendered = source; pane.textContent = 'Nothing to preview yet.'; return; } pane.textContent = 'Rendering...'; fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, body: JSON.stringify({ markdown: source }) }) .then(function (res) { if (!res.ok) { throw new Error('preview failed'); } return res.json(); }) .then(function (data) { state.rendered = source; pane.innerHTML = data.html; }) .catch(function () { // Deliberately not cached: the next flip should try again. pane.textContent = 'Preview unavailable. The content is unchanged.'; }); } function bind(wrapper) { // `data-ready` is both the CSS gate and the bound marker, which is what // keeps a second pass over the same document from wiring twice. if (wrapper.hasAttribute('data-ready')) { return; } var area = wrapper.querySelector('textarea[data-format="markdown"]'); var pane = wrapper.querySelector('[data-editor-preview]'); if (!area || !pane) { return; } var state = { rendered: null }; wrapper.querySelectorAll('[data-editor-mode]').forEach(function (button) { button.addEventListener('click', function () { var mode = button.dataset.editorMode; setMode(wrapper, mode); if (mode === 'preview') { renderPreview(area, pane, state); } }); }); setMode(wrapper, 'write'); wrapper.setAttribute('data-ready', ''); } function scan(root) { (root || document).querySelectorAll('[data-format="markdown"]').forEach(function (node) { // The mark sits on the wrapper and on the control it holds. Only // the wrapper has chrome to bind. if (node.tagName !== 'TEXTAREA') { bind(node); } }); } scan(document); document.body.addEventListener('htmx:after:settle', function (e) { scan(e.target); }); })();