diff options
| author | grm <grm@eyesin.space> | 2026-09-18 19:34:35 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-18 19:34:35 +0300 |
| commit | 96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f (patch) | |
| tree | e7851ddadf2ed776ab24a1bad4d446b7c48567db /internal/web/templates/dashboard | |
| parent | 63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d (diff) | |
| download | blogspace-96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f.tar.gz blogspace-96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f.tar.bz2 blogspace-96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f.zip | |
Show a live preview of the header on the Design tab's Menu card
The menu options had grown to a dozen switches with nothing to look at
but the blog after a save. A new POST /b/{sub}/design/preview reads the
form as a save would and renders the header alone — the real blog.css
and theme CSS, the modules in their order, the menu as edited — into a
sandboxed frame that refreshes shortly after every change, with a Wide
screen / Phone toggle. Nothing is stored.
The frame keeps the dashboard's origin (allow-same-origin, no scripts)
so the blog's logo and fonts, linked through /b/<sub>/media since the
root host's /media is the root blog's, get the session cookie; that is
safe because no header module is owner HTML. The frame runs no scripts,
so the page's own script folds the menu in it the way the blog does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/templates/dashboard')
| -rw-r--r-- | internal/web/templates/dashboard/design.html | 48 |
1 files changed, 47 insertions, 1 deletions
diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html index 0b27e17..039eabe 100644 --- a/internal/web/templates/dashboard/design.html +++ b/internal/web/templates/dashboard/design.html @@ -149,6 +149,17 @@ </div> <p class="muted small nojs">{{t "A page or link filled in here is added when you save."}}</p> <h3>{{t "How the menu looks"}}</h3> + {{/* The header as the form would save it, rendered by the server into a + sandboxed frame. allow-same-origin (never allow-scripts) so the frame + can load the blog's logo and fonts through /b/<sub>/media, which need the + session cookie: an opaque origin would not send it. Safe because the + header is never owner HTML — every header module kind is escaped by the + templates — and nothing runs without scripts. */}} + <div class="navpreview jsonly"> + <p class="muted small">{{t "Preview"}}: <button type="button" class="mini" data-pv="wide" aria-pressed="true">{{t "Wide screen"}}</button> <button type="button" class="mini" data-pv="phone" aria-pressed="false">{{t "Phone"}}</button> <span class="pvstatus"></span></p> + <iframe class="navpreview-frame" sandbox="allow-same-origin" scrolling="no" title="{{t "Preview"}}"></iframe> + <p class="muted small">{{t "Your header with the menu as set here; the phone view shows the folded menu. Nothing is saved until you press Save."}}</p> + </div> <div class="row"> <label>{{t "Alignment"}} <span class="muted">({{t "menu in the header"}})</span><br><select name="nav_align"> <option value="left"{{if eq $t.NavAlign "left"}} selected{{end}}>{{t "Left"}}</option> @@ -348,12 +359,47 @@ var next = +form.getAttribute("data-next") || 0, dirty = false, sending = false; function q(sel, el) { return (el || document).querySelector(sel); } function qa(sel, el) { return Array.prototype.slice.call((el || document).querySelectorAll(sel)); } - function touch() { dirty = true; q(".unsaved").hidden = false; } + function touch() { dirty = true; q(".unsaved").hidden = false; schedule(); } form.addEventListener("input", touch); form.addEventListener("change", touch); window.addEventListener("beforeunload", function (e) { if (dirty && !sending) { e.preventDefault(); e.returnValue = ""; } }); q("#discard").addEventListener("click", function (e) { if (dirty && !confirm({{t "Throw away your unsaved changes?"}})) e.preventDefault(); else dirty = false; }); + // ---- the header preview: the server renders the form as it would save it + var frame = q(".navpreview-frame"), pvTimer, pvBusy = false, pvAgain = false; + function preview() { + if (!frame || !window.fetch || !window.FormData) return; + if (pvBusy) { pvAgain = true; return; } // one at a time; the last change wins + pvBusy = true; + var fd = new FormData(form); + qa("input[type=file]", form).forEach(function (i) { fd.delete(i.name); }); // never upload while typing + fetch("/b/" + form.getAttribute("data-sub") + "/design/preview", { method: "POST", body: fd, credentials: "same-origin" }) + .then(function (r) { if (!r.ok) throw new Error(r.statusText); return r.text(); }) + .then(function (html) { frame.srcdoc = html; q(".pvstatus").textContent = ""; }) + .catch(function () { q(".pvstatus").textContent = {{t "Preview unavailable."}}; }) + .then(function () { pvBusy = false; if (pvAgain) { pvAgain = false; preview(); } }); + } + function schedule() { clearTimeout(pvTimer); pvTimer = setTimeout(preview, 300); } + function fitFrame() { // the frame runs no scripts, so fold the menu the way the blog's own script does, and size the frame to the header + try { + var d = frame.contentDocument, h = d.querySelector(".site-header.nav-fold"), links = h ? h.querySelectorAll(".site-nav .nav-inner a") : []; + if (links.length) { + h.classList.remove("nav-auto"); h.classList.remove("nav-folded"); + for (var i = 1; i < links.length; i++) if (links[i].offsetTop !== links[0].offsetTop) { h.classList.add("nav-folded"); break; } + } + frame.style.height = d.body.scrollHeight + "px"; + } catch (e) {} + } + if (frame) { + frame.addEventListener("load", function () { fitFrame(); try { frame.contentDocument.addEventListener("change", fitFrame); } catch (e) {} }); // change: the fold button opened or closed the panel + qa("[data-pv]").forEach(function (b) { b.addEventListener("click", function () { + frame.classList.toggle("phone", b.getAttribute("data-pv") === "phone"); + qa("[data-pv]").forEach(function (o) { o.setAttribute("aria-pressed", String(o === b)); }); + fitFrame(); + }); }); + preview(); + } + // ---- module and menu rows: order, remove, add function rows(list) { return qa(".modrow", list); } function refresh(list) { // the empty note and the "add" control of a one-module area |
