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>