git / moonbite-network / templates/partials/forum_script.html · raw
<div class="fx-toast" id="fx-toast" role="status" aria-live="polite"></div><script type="application/json" id="fx-emoji-data" nonce="{{ csp_nonce }}">{{ forum_emoji_picker()|tojson }}</script><script nonce="{{ csp_nonce }}">(function () { "use strict"; var KEY = "mb-forum-name"; var toastEl = document.getElementById("fx-toast"), toastT; function toast(msg) { if (!toastEl) return; toastEl.textContent = msg; toastEl.classList.add("on"); clearTimeout(toastT); toastT = setTimeout(function () { toastEl.classList.remove("on"); }, 1800); } /* remember the display name between posts (this browser only) */ var saved = ""; try { saved = localStorage.getItem(KEY) || ""; } catch (e) {} function fillNames(root) { root.querySelectorAll("input[data-name]").forEach(function (inp) { if (saved && !inp.value) inp.value = saved; }); } fillNames(document); document.addEventListener("submit", function (e) { var form = e.target; if (!form.classList || !form.classList.contains("fx-form")) return; var n = form.querySelector("input[data-name]"); try { if (n && n.value.trim()) localStorage.setItem(KEY, n.value.trim()); } catch (err) {} var b = form.querySelector("button[type=submit]"); if (b) { b.disabled = true; b.textContent = "Posting…"; } }); /* character counters + Ctrl/Cmd+Enter to post */ function wireTextarea(ta) { var max = Number(ta.getAttribute("data-max")); var out = ta.form && ta.form.querySelector(".count"); function paint() { if (!out) return; var left = max - ta.value.length; out.textContent = left < 1000 ? left.toLocaleString("en-US") + " characters left" : ""; out.classList.toggle("low", left < 200); } ta.addEventListener("input", paint); paint(); ta.addEventListener("keydown", function (e) { if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && ta.form) { e.preventDefault(); if (ta.form.reportValidity()) ta.form.requestSubmit ? ta.form.requestSubmit() : ta.form.submit(); } }); } document.querySelectorAll("textarea[data-max]").forEach(wireTextarea); /* open the new-post composer from links to #new */ var compose = document.getElementById("new"); function openCompose() { if (compose && location.hash === "#new") { compose.open = true; var t = compose.querySelector("input[name=title]"); if (t) t.focus(); } } window.addEventListener("hashchange", openCompose); openCompose(); /* votes: optimistic, then reconciled with the server's score */ function fmt(n) { return Math.abs(n) >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, "") + "k" : String(n); } function paintVote(box, mine, score) { box.dataset.mine = mine; box.dataset.score = score; box.querySelector(".up").setAttribute("aria-pressed", mine === 1 ? "true" : "false"); box.querySelector(".dn").setAttribute("aria-pressed", mine === -1 ? "true" : "false"); box.classList.toggle("v1", mine === 1); box.querySelector(".sc").textContent = fmt(score); } document.querySelectorAll(".fx-vote").forEach(function (b) { paintVote(b, Number(b.dataset.mine), Number(b.dataset.score)); }); function castVote(btn) { var box = btn.closest(".fx-vote"); if (box.classList.contains("busy")) return; var was = Number(box.dataset.mine), score = Number(box.dataset.score); var want = btn.classList.contains("up") ? 1 : -1; var next = was === want ? 0 : want; paintVote(box, next, score - was + next); box.classList.add("busy"); fetch("/api/discussions/vote", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: box.dataset.kind, id: Number(box.dataset.id), value: next }) }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }) .then(function (res) { if (!res.ok || res.d.status !== "success") throw new Error(res.d.message || "vote failed"); paintVote(box, res.d.mine, res.d.score); }) .catch(function () { paintVote(box, was, score); toast("Vote didn't go through. Try again in a moment."); }) .then(function () { box.classList.remove("busy"); }); } /* inline reply under a comment */ var tpl = document.getElementById("fx-reply-tpl"); function openReply(btn) { var c = btn.closest(".fx-c"), slot = c && c.querySelector(":scope > .cm > .slot"); if (!slot || !tpl) return; if (slot.firstElementChild) { var ta0 = slot.querySelector("textarea"); if (ta0) ta0.focus(); return; } var node = tpl.content.cloneNode(true); node.querySelector("input[name=parent_id]").value = c.dataset.id; node.querySelector("textarea").setAttribute("placeholder", "Reply to " + c.dataset.author); slot.appendChild(node); fillNames(slot); var ta = slot.querySelector("textarea"); wireTextarea(ta); ta.focus(); } /* emoji picker: one shared panel, opened under whichever Emoji button was pressed */ var picker = null, pickerFor = null, emojiData = []; try { emojiData = JSON.parse(document.getElementById("fx-emoji-data").textContent); } catch (err) {} function buildPicker() { picker = document.createElement("div"); picker.className = "fx-picker"; picker.hidden = true; picker.setAttribute("role", "dialog"); picker.setAttribute("aria-label", "Emoji"); var search = document.createElement("input"); search.className = "ds-input"; search.type = "search"; search.placeholder = "Search emoji"; search.setAttribute("aria-label", "Search emoji"); var grid = document.createElement("div"); grid.className = "grid"; var foot = document.createElement("div"); foot.className = "foot"; foot.textContent = "Pick an emoji"; emojiData.forEach(function (g) { var h = document.createElement("h3"); h.textContent = g.name; grid.appendChild(h); var row = document.createElement("div"); row.className = "row"; g.items.forEach(function (it) { var b = document.createElement("button"); b.type = "button"; b.dataset.insert = it.c; b.dataset.name = it.s; b.title = ":" + it.s + ":"; var img = document.createElement("img"); img.src = "/static/emoji/" + it.k; img.alt = it.c; b.appendChild(img); row.appendChild(b); b.addEventListener("mouseenter", function () { foot.textContent = ":" + it.s + ":"; }); }); grid.appendChild(row); }); var none = document.createElement("p"); none.className = "none"; none.textContent = "No emoji match."; none.hidden = true; grid.appendChild(none); search.addEventListener("input", function () { var q = search.value.trim().toLowerCase().replace(/:/g, ""), any = false; grid.querySelectorAll(".row").forEach(function (row) { var shown = 0; row.querySelectorAll("button").forEach(function (b) { var m = !q || b.dataset.name.indexOf(q) >= 0; b.hidden = !m; if (m) shown++; }); row.hidden = !shown; row.previousElementSibling.hidden = !shown || !!q; if (shown) any = true; }); none.hidden = any; }); picker.appendChild(search); picker.appendChild(grid); picker.appendChild(foot); document.body.appendChild(picker); } function openPicker(btn) { if (!emojiData.length) return; if (!picker) buildPicker(); var form = btn.closest("form"); pickerFor = form && form.querySelector("textarea"); if (!picker.hidden && picker._btn === btn) { closePicker(); return; } picker._btn = btn; var r = btn.getBoundingClientRect(), w = Math.min(344, innerWidth - 32); picker.style.left = Math.max(16, Math.min(r.left + scrollX, scrollX + innerWidth - w - 16)) + "px"; picker.style.top = (r.bottom + scrollY + 8) + "px"; picker.hidden = false; var s = picker.querySelector("input"); s.value = ""; s.dispatchEvent(new Event("input")); s.focus(); } function closePicker() { if (picker) picker.hidden = true; } function insertEmoji(text) { var ta = pickerFor; if (!ta) return; var a = ta.selectionStart || 0, b = ta.selectionEnd || 0; ta.value = ta.value.slice(0, a) + text + ta.value.slice(b); ta.focus(); ta.setSelectionRange(a + text.length, a + text.length); ta.dispatchEvent(new Event("input")); } document.addEventListener("keydown", function (e) { if (e.key === "Escape" && picker && !picker.hidden) { closePicker(); if (picker._btn) picker._btn.focus(); } }); document.addEventListener("click", function (e) { var t = e.target.closest ? e.target : null; if (!t) return; var eb = t.closest("[data-emoji]"); if (eb) { e.preventDefault(); openPicker(eb); return; } var ins = t.closest(".fx-picker [data-insert]"); if (ins) { e.preventDefault(); insertEmoji(ins.dataset.insert); return; } if (picker && !picker.hidden && !t.closest(".fx-picker")) closePicker(); var v = t.closest(".fx-vote button"); if (v) { e.preventDefault(); castVote(v); return; } var r = t.closest("[data-reply]"); if (r) { e.preventDefault(); openReply(r); return; } var cancel = t.closest("[data-cancel]"); if (cancel) { e.preventDefault(); var s = cancel.closest(".slot"); if (s) s.textContent = ""; return; } var col = t.closest("[data-collapse]"); if (col) { e.preventDefault(); var c = col.closest(".fx-c"), shut = !c.classList.contains("collapsed"); c.classList.toggle("collapsed", shut); c.querySelectorAll(":scope > .rail [data-collapse], :scope > .cm > .ch [data-collapse]").forEach(function (b) { b.setAttribute("aria-expanded", shut ? "false" : "true"); }); return; } /* report: pick a reason inline, then send it */ var rp = t.closest("[data-report]"); if (rp) { e.preventDefault(); var host = rp.closest(".fx-c") ? rp.closest(".fx-c").querySelector(":scope > .cm > .slot") : rp.closest("article").querySelector(".slot"); if (!host) return; if (host.querySelector(".fx-reportbox")) { host.textContent = ""; return; } host.textContent = ""; var box = document.createElement("div"); box.className = "fx-reportbox"; box.appendChild(document.createTextNode("Why report this?")); var parts = rp.getAttribute("data-report").split(":"); [["spam", "Spam"], ["scam", "Scam or phishing"], ["abuse", "Abuse"], ["secret", "Shares a seed or key"], ["other", "Other"]].forEach(function (r) { var b = document.createElement("button"); b.type = "button"; b.textContent = r[1]; b.addEventListener("click", function () { fetch("/api/discussions/report", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: parts[0], id: Number(parts[1]), reason: r[0] }) }) .then(function (res) { if (!res.ok) throw 0; host.textContent = ""; toast("Thanks. A moderator will look at it."); }) .catch(function () { toast("Report didn't go through. Try again in a moment."); }); }); box.appendChild(b); }); host.appendChild(box); return; } var sh = t.closest("[data-share]"); if (sh) { e.preventDefault(); var url = new URL(sh.getAttribute("data-share"), location.href).href; if (navigator.clipboard) navigator.clipboard.writeText(url).then(function () { toast("Link copied"); }, function () { toast(url); }); else toast(url); } });})();</script>