diff options
Diffstat (limited to 'internal/web/templates')
| -rw-r--r-- | internal/web/templates/blog/preview.html | 18 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/design.html | 48 | ||||
| -rw-r--r-- | internal/web/templates/layouts/blog.html | 6 |
3 files changed, 68 insertions, 4 deletions
diff --git a/internal/web/templates/blog/preview.html b/internal/web/templates/blog/preview.html new file mode 100644 index 0000000..a87e410 --- /dev/null +++ b/internal/web/templates/blog/preview.html @@ -0,0 +1,18 @@ +{{/* The design page's preview of the header: a page of its own for the + sandboxed frame — the real blog.css and theme CSS, and the header as the + form would save it. Only "preview" is ever executed; the empty content + block keeps the layout's reference satisfied. */}} +{{define "content"}}{{end}} +{{define "preview"}}<!DOCTYPE html> +<html lang="{{.Lang}}"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>{{t "Preview"}}</title> +<link rel="stylesheet" href="/static/blog.css"> +<style>{{css .Data.css}}</style> +</head> +<body class="{{.Data.theme.NavClass}}{{with .Data.theme.TitleRuleClass}} {{.}}{{end}}"> +{{template "siteheader" .}} +</body> +</html>{{end}} 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 diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html index d55512a..71a62b0 100644 --- a/internal/web/templates/layouts/blog.html +++ b/internal/web/templates/layouts/blog.html @@ -48,7 +48,7 @@ which drops nav-auto, the no-script fallback blog.css folds on phones. */}} {{define "siteheader"}}{{$l := .Data.layout}}{{$t := .Data.theme}}<div class="site-header{{if $l.Fold}} nav-fold nav-auto{{end}}{{if eq $t.HeaderAlign "center"}} header-center{{end}}"> {{if $l.Fold}}<input type="checkbox" id="nav-toggle" class="nav-toggle" aria-label="{{t "Menu"}}">{{end}} - {{if $t.HeaderImage}}<div class="header-image"><a href="/"><img src="/media/{{$t.HeaderImage}}" alt=""></a></div>{{end}} + {{if $t.HeaderImage}}<div class="header-image"><a href="/"><img src="{{$.Media}}/{{$t.HeaderImage}}" alt=""></a></div>{{end}} {{range $i, $m := $l.Header}}{{template "module" (dict "m" $m "root" $ "burger" (and $l.Fold (eq $i $l.BurgerAt)))}}{{end}} </div>{{if $l.Fold}}<script>(function () { var h = document.querySelector && document.querySelector(".site-header.nav-fold"), cb = document.getElementById("nav-toggle"); @@ -86,8 +86,8 @@ <h1 class="site-title"><a href="/">{{$r.Blog.Title}}</a></h1> {{if $r.Blog.Tagline}}<p class="tagline">{{$r.Blog.Tagline}}</p>{{end}} </div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}{{if $b}}{{template "burger" $t}}{{end}}</div> -{{- else if eq $m.Kind "logo"}}{{if $t.Logo}}{{if eq $m.Area "header"}}<div class="wrap header-logo{{with $rc}} {{.}}{{end}}{{if $b}} has-burger{{end}}">{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}<div class="brand"><a href="/"><img class="logo" src="/media/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}{{if $b}}{{template "burger" $t}}{{end}}</div> -{{else}}<div class="mod mod-logo"><a href="/"><img class="logo" src="/media/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{end}}{{end}} +{{- else if eq $m.Kind "logo"}}{{if $t.Logo}}{{if eq $m.Area "header"}}<div class="wrap header-logo{{with $rc}} {{.}}{{end}}{{if $b}} has-burger{{end}}">{{template "rule" (dict "before" true "rc" $rc "theme" $t)}}<div class="brand"><a href="/"><img class="logo" src="{{$r.Media}}/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{template "rule" (dict "before" false "rc" $rc "theme" $t)}}{{if $b}}{{template "burger" $t}}{{end}}</div> +{{else}}<div class="mod mod-logo"><a href="/"><img class="logo" src="{{$r.Media}}/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{end}}{{end}} {{- else if eq $m.Kind "menu"}}{{if eq $m.Area "header"}}<div class="site-nav{{if $b}} has-burger{{end}}"><div class="wrap nav-inner">{{if $b}}{{template "burger" $t}}{{end}}{{if $r.Data.layout.HeaderSearch}}{{template "searchform" $r}}{{end}}{{template "menulinks" $r}}</div></div> {{else}}<div class="mod site-nav mod-menu">{{template "modtitle" $m}}<div class="nav-inner">{{template "menulinks" $r}}</div></div>{{end}} {{- else if eq $m.Kind "recent"}}<div class="mod mod-recent">{{template "modtitle" $m}} |
