fuzzyworld

fuzzyworld-tools / files / server / site-blog / blog.js

blog.js

Blame: the commit that last changed each line · file · history

0e603cf7 2026-10-31 Pavel Safronov1/* The blog's two small conveniences: a light/dark switch and a reading-progress bar. No dependencies.
2 The choice is kept in localStorage; with none, the system setting decides (see _shared.css).
3 Loaded in <head> so the saved theme is applied before the first paint. */
4(function () {
5 var root = document.documentElement, KEY = "fw-theme", saved = null;
6 try { saved = localStorage.getItem(KEY); } catch (e) {}
7 if (saved === "light" || saved === "dark") root.setAttribute("data-theme", saved);
8
9 function effective() {
10 var t = root.getAttribute("data-theme");
11 return t || (window.matchMedia && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
12 }
13
14 document.addEventListener("DOMContentLoaded", function () {
15 var bar = document.querySelector("header.site .bar");
16 if (bar) {
17 var b = document.createElement("button");
18 b.type = "button";
19 b.className = "theme-toggle";
20 var label = function () {
21 var dark = effective() === "dark";
22 b.textContent = dark ? "☀" : "☾";
23 var text = dark ? "Switch to the light theme" : "Switch to the dark theme";
24 b.title = text;
25 b.setAttribute("aria-label", text);
26 };
27 b.addEventListener("click", function () {
28 var next = effective() === "dark" ? "light" : "dark";
29 root.setAttribute("data-theme", next);
30 try { localStorage.setItem(KEY, next); } catch (e) {}
31 label();
32 });
33 label();
34 bar.appendChild(b);
35 if (window.matchMedia) { // the system setting changed: the label follows unless the reader chose a theme
36 var mq = matchMedia("(prefers-color-scheme: dark)");
37 if (mq.addEventListener) mq.addEventListener("change", label); else if (mq.addListener) mq.addListener(label);
38 }
39 }
40
41 var post = document.querySelector(".post-body");
42 if (post) {
43 var p = document.createElement("div");
44 p.className = "progress";
45 p.setAttribute("aria-hidden", "true");
46 document.body.appendChild(p);
47 var update = function () { // 0 at the top of the page, 1 at the bottom
48 var room = document.documentElement.scrollHeight - window.innerHeight;
49 var done = room > 0 ? window.scrollY / room : 0;
50 p.style.width = Math.max(0, Math.min(1, done)) * 100 + "%";
51 };
52 window.addEventListener("scroll", update, { passive: true });
53 window.addEventListener("resize", update);
54 update();
55 }
e9fdc5db 2026-11-01 Pavel Safronov56
57 comments();
0e603cf7 2026-10-31 Pavel Safronov58 });
e9fdc5db 2026-11-01 Pavel Safronov59
60 /* Reader comments: /_comments/<post>.json is written by the server. Missing (404), broken or not a list: nothing shown.
61 Items are built with createElement/textContent only. Comments already in the page (imported, id="comment-N") are skipped. */
62 function comments() {
63 var started = Date.now();
64 var sections = document.querySelectorAll(".comments-section[data-src]");
65 Array.prototype.forEach.call(sections, function (sec) {
66 var form = sec.querySelector("form.comment-form");
67 if (form) form.addEventListener("submit", function () {
68 var f = form.elements.elapsed;
69 if (f) f.value = String(Date.now() - started);
70 });
71 if (!window.fetch) return;
72 fetch(sec.getAttribute("data-src"), { credentials: "omit", headers: { Accept: "application/json" } })
73 .then(function (r) { return r.ok ? r.json() : null; })
74 .then(function (list) { if (Array.isArray(list)) show(sec, list); })
75 .catch(function () {});
76 });
77 }
78
79 function cid(c) { return c && c.id != null ? String(c.id).replace(/[^A-Za-z0-9_-]/g, "") : ""; }
80
81 function show(sec, list) {
82 var box = sec.querySelector(".comments-live") || sec;
83 var fresh = list.filter(function (c) {
84 return c && typeof c === "object" && cid(c) && !document.getElementById("comment-" + cid(c));
85 });
86 fresh.sort(function (a, b) { return String(a.date || "") < String(b.date || "") ? -1 : String(a.date || "") > String(b.date || "") ? 1 : 0; });
87 var made = Object.create(null);
88 fresh.forEach(function (c) { made[cid(c)] = item(c); });
89 var top = document.createElement("ul");
90 top.className = "comments";
91 fresh.forEach(function (c) {
92 var li = made[cid(c)], pid = c.parent ? cid({ id: c.parent }) : "";
93 var parent = pid && pid !== cid(c) ? (made[pid] || document.getElementById("comment-" + pid)) : null;
94 if (!parent) { top.appendChild(li); return; }
95 var ul = null;
96 for (var n = parent.firstChild; n; n = n.nextSibling) if (n.nodeName === "UL") ul = n;
97 if (!ul) { ul = document.createElement("ul"); ul.className = "comments"; parent.appendChild(ul); }
98 ul.appendChild(li);
99 });
100 if (top.firstChild) box.appendChild(top);
101 }
102
103 function item(c) {
104 var li = document.createElement("li");
105 li.className = "comment";
106 li.id = "comment-" + cid(c);
107 var meta = document.createElement("p");
108 meta.className = "meta";
109 var who = document.createElement("strong");
110 who.textContent = String(c.author == null ? "" : c.author);
111 meta.appendChild(who);
112 meta.appendChild(document.createTextNode(" · " + String(c.date == null ? "" : c.date).slice(0, 10)));
113 var body = document.createElement("p");
114 String(c.text == null ? "" : c.text).replace(/\r\n/g, "\n").split("\n").forEach(function (line, i) {
115 if (i) { body.appendChild(document.createElement("br")); body.appendChild(document.createTextNode("\n")); }
116 body.appendChild(document.createTextNode(line));
117 });
118 li.appendChild(meta);
119 li.appendChild(body);
120 return li;
121 }
0e603cf7 2026-10-31 Pavel Safronov122})();