aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/templates/dashboard/design.html
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/templates/dashboard/design.html')
-rw-r--r--internal/web/templates/dashboard/design.html48
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