/* Progressive enhancement only: every page works without this file. */
(function () {
"use strict";
/* The band's active tab: the longest link that prefixes the path. */
var p = location.pathname, best = null;
document.querySelectorAll(".band a").forEach(function (a) {
var h = a.getAttribute("href");
if (h !== "/" && (p === h || p.indexOf(h + "/") === 0) && (!best || h.length > best.getAttribute("href").length)) best = a;
});
if (best) best.classList.add("on");
/* Off-the-shelf editors, loaded only where a page asks for them:
CodeMirror 6 over textarea.code, EasyMDE over textarea.md. */
function load(src, css) {
return new Promise(function (ok) {
if (css) { var l = document.createElement("link"); l.rel = "stylesheet"; l.href = css; document.head.appendChild(l); }
var s = document.createElement("script"); s.src = src; s.onload = ok; document.head.appendChild(s);
});
}
if (document.querySelector("textarea.code")) load("/static/vendor/editor.js");
var mds = document.querySelectorAll("textarea.md");
if (mds.length) load("/static/vendor/easymde.min.js", "/static/vendor/easymde.min.css").then(function () {
var E = window.EasyMDE;
function b(name, action, text, title) { return { name: name, action: action, text: text, title: title, className: "mde-btn" }; }
var toolbar = [b("bold", E.toggleBold, "B", "Bold"), b("italic", E.toggleItalic, "I", "Italic"), b("heading", E.toggleHeadingSmaller, "H", "Heading"), "|",
b("code", E.toggleCodeBlock, ">", "Code"), b("quote", E.toggleBlockquote, "“", "Quote"), b("ul", E.toggleUnorderedList, "•", "List"), b("ol", E.toggleOrderedList, "1.", "Numbered list"), "|",
b("link", E.drawLink, "link", "Link"), "|", b("preview", E.togglePreview, "preview", "Preview")];
mds.forEach(function (t) { t._mde = new E({ element: t, spellChecker: false, status: false, forceSync: true, autoDownloadFontAwesome: false, toolbar: toolbar }); });
/* An editor shown later (a :target edit form) lays itself out again. */
window.addEventListener("hashchange", function () { mds.forEach(function (t) { if (t._mde) setTimeout(function () { t._mde.codemirror.refresh(); }, 0); }); });
});
/* Mermaid diagrams (```mermaid blocks in Markdown): drawn by the hub's own
copy of Mermaid, loaded only on pages that have one, in strict mode (no
scripts or HTML from the diagram), in the page's theme. A diagram that
does not parse stays as its source, with Mermaid's reason under it. */
var diagrams = document.querySelectorAll("pre > code.language-mermaid");
if (diagrams.length) load("/static/vendor/mermaid.min.js").then(function () {
var m = window.mermaid, dark = document.documentElement.dataset.theme === "dark";
m.initialize({ startOnLoad: false, securityLevel: "strict", suppressErrorRendering: true, theme: dark ? "dark" : "default" });
diagrams.forEach(function (code, i) {
var pre = code.parentNode, source = code.textContent;
m.render("mermaid-" + i, source).then(function (r) {
var fig = document.createElement("figure");
fig.className = "mermaid-diagram";
fig.innerHTML = r.svg;
fig.setAttribute("aria-label", "Diagram");
pre.replaceWith(fig);
}).catch(function (e) {
var note = document.createElement("p");
note.className = "small warn";
note.textContent = "This diagram could not be drawn: " + ((e && e.message) || e);
pre.after(note);
});
});
});
/* Report frames (#127): a report's page says how tall it is, and the
frame grows to fit; the frame is told the theme, at start and on a
change. Only messages from the frame itself are taken. */
var frames = document.querySelectorAll("iframe.rep-frame");
var themeToFrames = function () {
frames.forEach(function (f) { if (f.contentWindow) f.contentWindow.postMessage({ oxygenhubTheme: document.documentElement.dataset.theme || "dark" }, "*"); });
};
if (frames.length) window.addEventListener("message", function (e) {
frames.forEach(function (f) {
if (e.source !== f.contentWindow) return;
var r = e.data && e.data.oxygenhubReport;
if (!r) return;
if (r.ready) themeToFrames();
if (typeof r.height === "number" && r.height > 0) {
f.style.height = Math.min(Math.max(r.height, 120), 40000) + "px";
f.classList.add("sized");
}
});
});
/* Report frames ask for runs (#127, RP4): report.run() is the run on
screen, report.history(dataset) that file of data/ across the runs,
oldest first. Answered only to the frame itself, from the files of
this report's own runs. */
if (frames.length) window.addEventListener("message", function (e) {
frames.forEach(function (f) {
if (e.source !== f.contentWindow) return;
var ask = e.data && e.data.oxygenhubAsk;
if (!ask) return;
var runs = []; try { runs = JSON.parse(f.dataset.reportRuns || "[]"); } catch (_) {}
var current = Number(f.dataset.reportCurrent);
var reply = function (body) { f.contentWindow.postMessage({ oxygenhubAnswer: { n: ask.n, value: body.value, error: body.error } }, "*"); };
var meta = function (r) { return { run: r.run, bookmark: r.bookmark, created_ms: r.created_ms, summary: r.summary, current: r.id === current }; };
if (ask.what === "run") {
var r = runs.filter(function (x) { return x.id === current; })[0];
reply({ value: r ? meta(r) : null });
} else if (ask.what === "history" && typeof ask.dataset === "string" && /^[A-Za-z0-9._+-]+(\/[A-Za-z0-9._+-]+)*$/.test(ask.dataset)) {
Promise.all(runs.map(function (r) {
return fetch(r.files + "/data/" + ask.dataset).then(function (res) { return res.ok ? res.text() : null; }).catch(function () { return null; })
.then(function (text) { var m = meta(r); m.text = text; return m; });
})).then(function (all) { reply({ value: all.filter(function (x) { return x.text !== null; }) }); });
} else {
reply({ error: "ask for run, or history with a file name under data/" });
}
});
});
/* The report timeline (#127, RP4): arrow keys step through the runs,
Play steps on by itself at the chosen speed, carried in the URL. */
var tl = document.querySelector("[data-timeline]");
if (tl) {
var stops = Array.prototype.slice.call(tl.querySelectorAll("[data-tl-stop]"));
var at = stops.findIndex(function (a) { return a.classList.contains("on"); });
var go = function (i, play) {
if (i < 0 || i >= stops.length) return;
var u = new URL(stops[i].href, location.href);
if (play) u.searchParams.set("play", play); else u.searchParams.delete("play");
location.href = u.toString();
};
document.addEventListener("keydown", function (e) {
if (e.target.closest("input, select, textarea, [contenteditable]")) return;
if (e.key === "ArrowLeft") { e.preventDefault(); go(at - 1, tl.dataset.play); }
if (e.key === "ArrowRight") { e.preventDefault(); go(at + 1, tl.dataset.play); }
});
var speed = tl.querySelector("[data-tl-speed]"), button = tl.querySelector("[data-tl-play]");
if (button) button.addEventListener("click", function () {
if (tl.dataset.play) { go(at, null); return; }
go(at >= stops.length - 1 ? 0 : at + 1, speed ? speed.value : "2");
});
if (speed) speed.addEventListener("change", function () { if (tl.dataset.play) go(at, speed.value); });
if (tl.dataset.play) {
var next = at + 1;
if (next < stops.length) setTimeout(function () { go(next, tl.dataset.play); }, Number(tl.dataset.play) * 1000);
else setTimeout(function () { go(at, null); }, Number(tl.dataset.play) * 1000);
}
}
/* Theme switch in the header: dark, light, auto (follows the system); kept in the theme cookie. */
var toggle = document.querySelector("[data-theme-toggle]");
if (toggle) {
var root = document.documentElement, order = ["light", "dark", "auto"], glyph = { dark: "◐", light: "☀", auto: "◑" };
var show = function () {
var p = root.dataset.themePref || "light";
toggle.title = "Theme: " + p + " (click to change)";
toggle.setAttribute("aria-label", toggle.title);
toggle.firstElementChild.textContent = glyph[p];
};
show();
toggle.addEventListener("click", function () {
var p = order[(order.indexOf(root.dataset.themePref || "light") + 1) % order.length];
document.cookie = "theme=" + p + "; path=/; max-age=31536000; samesite=lax";
root.dataset.themePref = p;
root.dataset.theme = p === "auto" ? (window.matchMedia && matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
show();
themeToFrames();
});
}
/* Diffs: unified or split (remembered in the diff cookie), collapse all. */
var diffs = document.querySelectorAll("table.code.diff");
if (diffs.length) {
var pane = function (tr, col, side) {
var n = document.createElement("td"), t = document.createElement("td");
n.className = "n"; t.className = "t" + (side === "right" ? " mid" : "");
if (!tr) { n.className += " none"; t.className += " none"; return [n, t]; }
var kind = tr.classList.contains("add") ? "add" : tr.classList.contains("del") ? "del" : "ctx";
if (kind !== "ctx") { n.className += " " + kind; t.className += " " + kind; }
n.textContent = tr.cells[col].textContent;
t.innerHTML = tr.cells[3].innerHTML;
// Line comments belong to the projected (right) side only.
if (side === "left") t.querySelectorAll(".linecomment").forEach(function (d) { d.remove(); });
return [n, t];
};
var split = function (table) {
var s = document.createElement("table");
s.className = "code diff split";
s.innerHTML = '
';
var rows = [].slice.call(table.rows), i = 0;
var add = function (l, r) {
var tr = s.insertRow();
pane(l, 0, "left").concat(pane(r, 1, "right")).forEach(function (c) { tr.appendChild(c); });
};
while (i < rows.length) {
var r = rows[i];
if (r.classList.contains("del") || r.classList.contains("add")) {
var dels = [], adds = [];
while (i < rows.length && rows[i].classList.contains("del")) dels.push(rows[i++]);
while (i < rows.length && rows[i].classList.contains("add")) adds.push(rows[i++]);
for (var k = 0; k < Math.max(dels.length, adds.length); k++) add(dels[k], adds[k]);
continue;
}
if (r.classList.contains("gap") || r.querySelector("td.thread")) {
var tr = s.insertRow(), td = tr.insertCell();
tr.className = r.className; td.colSpan = 4; td.className = r.cells[r.cells.length - 1].className;
td.innerHTML = r.cells[r.cells.length - 1].innerHTML;
} else {
add(r, r);
}
i++;
}
return s;
};
var cookieMode = (document.cookie.match(/(?:^|; )diff=(split|unified)/) || [])[1] || "unified";
var apply = function (mode) {
diffs.forEach(function (t) {
if (mode === "split" && !t._split) { t._split = split(t); t.parentNode.insertBefore(t._split, t.nextSibling); }
t.hidden = mode === "split";
if (t._split) t._split.hidden = mode !== "split";
});
document.querySelectorAll("[data-diff-mode]").forEach(function (a) { a.classList.toggle("on", a.getAttribute("data-diff-mode") === mode); });
};
document.querySelectorAll("[data-diff-modes], [data-diff-collapse]").forEach(function (e) { e.hidden = false; });
document.querySelectorAll("[data-diff-mode]").forEach(function (a) {
a.addEventListener("click", function (ev) {
ev.preventDefault();
var mode = a.getAttribute("data-diff-mode");
document.cookie = "diff=" + mode + "; path=/; max-age=31536000; samesite=lax";
apply(mode);
});
});
document.querySelectorAll("[data-diff-collapse]").forEach(function (a) {
a.addEventListener("click", function (ev) {
ev.preventDefault();
var files = document.querySelectorAll("details.file-diff"), anyOpen = [].some.call(files, function (d) { return d.open; });
files.forEach(function (d) { d.open = !anyOpen; });
a.textContent = anyOpen ? "expand all" : "collapse all";
});
});
apply(cookieMode);
}
/* Settings pages: mark the section in view in the list of sections. */
var navLinks = document.querySelectorAll(".section-nav a[href^='#']");
if (navLinks.length && "IntersectionObserver" in window) {
var byId = {};
navLinks.forEach(function (a) { byId[a.getAttribute("href").slice(1)] = a; });
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting && byId[e.target.id]) {
navLinks.forEach(function (a) { a.classList.remove("on"); });
byId[e.target.id].classList.add("on");
}
});
}, { rootMargin: "0px 0px -70% 0px" });
Object.keys(byId).forEach(function (id) { var el = document.getElementById(id); if (el) io.observe(el); });
}
/* Copy buttons. */
document.querySelectorAll("[data-copy]").forEach(function (b) {
b.addEventListener("click", function () {
navigator.clipboard.writeText(b.getAttribute("data-copy")).then(function () {
var t = b.textContent; b.textContent = "copied"; setTimeout(function () { b.textContent = t; }, 1200);
});
});
});
/* Tree rows: the last change per entry, fetched after the page paints. */
var tree = document.querySelector("table.tree[data-last]");
if (tree) {
var paths = [].map.call(tree.querySelectorAll("tr[data-path]"), function (r) { return r.getAttribute("data-path"); });
if (paths.length) {
fetch(tree.getAttribute("data-last") + "?paths=" + encodeURIComponent(paths.join(",")), { credentials: "same-origin" })
.then(function (r) { return r.json(); })
.then(function (rows) {
rows.forEach(function (c) {
var tr = tree.querySelector('tr[data-path="' + CSS.escape(c.path) + '"]');
if (!tr) return;
var a = tr.querySelector("td.last a"), t = tr.querySelector("td.age");
if (a) { a.textContent = c.title; a.href = tree.getAttribute("data-change") + c.hash; }
if (t) { t.textContent = c.ago; t.title = c.when; }
});
}).catch(function () {});
}
}
/* Line anchors: #L12 or #L12-L20 highlight a range and clicking a number selects. */
function mark() {
var m = location.hash.match(/^#L(\d+)(?:-L(\d+))?$/);
document.querySelectorAll("tr.hl").forEach(function (r) { r.classList.remove("hl"); });
if (!m) { showBar(0, 0); return; }
var a = +m[1], b = +(m[2] || m[1]);
for (var i = Math.min(a, b); i <= Math.max(a, b); i++) {
var row = document.getElementById("L" + i);
if (row) row.classList.add("hl");
}
var first = document.getElementById("L" + Math.min(a, b));
if (first && !mark.done) { first.scrollIntoView({ block: "center" }); mark.done = true; }
showBar(Math.min(a, b), Math.max(a, b));
}
/* The bar for a selected range: copy its text, or a permalink to it. */
var bar = document.querySelector(".linebar");
function showBar(a, b) {
if (!bar) return;
bar.hidden = !a;
if (!a) return;
bar.querySelector("[data-linebar-label]").textContent = a === b ? "Line " + a : "Lines " + a + "–" + b;
bar.setAttribute("data-range", a + "-" + b);
}
function flash(btn) { var t = btn.textContent; btn.textContent = "Copied"; setTimeout(function () { btn.textContent = t; }, 1200); }
if (bar) {
bar.querySelector("[data-copy-lines]").addEventListener("click", function (ev) {
var r = bar.getAttribute("data-range").split("-"), out = [];
for (var i = +r[0]; i <= +r[1]; i++) { var row = document.getElementById("L" + i); if (row) out.push(row.querySelector("td.t").textContent); }
navigator.clipboard.writeText(out.join("\n") + "\n").then(function () { flash(ev.target); });
});
var pl = bar.querySelector("[data-copy-permalink]");
if (pl) pl.addEventListener("click", function () {
navigator.clipboard.writeText(new URL(document.body.getAttribute("data-permalink"), location.href).href + location.hash).then(function () { flash(pl); });
});
}
function permalink() { var p = document.body.getAttribute("data-permalink"); return p ? p + location.hash : null; }
window.addEventListener("hashchange", mark);
mark();
document.querySelectorAll("table.code td.n").forEach(function (td) {
td.addEventListener("click", function (e) {
var n = td.parentNode.id;
if (e.shiftKey && location.hash.match(/^#L(\d+)/)) location.hash = location.hash.replace(/^#(L\d+).*/, "$1-" + n);
else location.hash = n;
});
});
/* Flow run page: statuses and new log lines while the run is live. */
var liveUrl = document.body.getAttribute("data-flow-live");
if (liveUrl) {
var tick = function () {
var after = [].map.call(document.querySelectorAll("details[data-job]"), function (d) {
return d.getAttribute("data-job") + ":" + d.querySelector("[data-log]").getAttribute("data-seq");
}).join(",");
fetch(liveUrl + "?after=" + after, { headers: { accept: "application/json" } })
.then(function (r) { return r.json(); })
.then(function (s) {
s.jobs.forEach(function (j) {
var cls = "st-" + j.status;
var node = document.querySelector("[data-job-node='" + j.id + "']");
if (node) {
node.setAttribute("class", "node " + cls);
node.querySelector("[data-glyph]").textContent = j.glyph;
node.querySelector("[data-meta]").textContent = j.status + " · " + j.duration;
}
var d = document.querySelector("details[data-job='" + j.id + "']");
if (!d) return;
d.className = "panel flow-job " + cls;
var g = d.querySelector("summary [data-glyph]");
g.className = "flow-glyph " + cls; g.textContent = j.glyph;
d.querySelector("summary [data-meta]").textContent = j.status + (j.exit_code !== null ? " · exit " + j.exit_code : "") + " · " + j.duration;
if (j.status === "running" || j.status === "failed") d.open = true;
var log = d.querySelector("[data-log]");
var stick = log.scrollTop + log.clientHeight >= log.scrollHeight - 4;
j.lines.forEach(function (l) {
var ln = document.createElement("span"); ln.className = "ln s-" + l.stream;
var t = document.createElement("span"); t.className = "t"; t.textContent = (l.t / 1000).toFixed(1) + "s";
ln.appendChild(t); ln.appendChild(document.createTextNode(l.text));
log.appendChild(ln); log.appendChild(document.createTextNode("\n"));
log.setAttribute("data-seq", l.seq);
});
if (stick && j.lines.length) log.scrollTop = log.scrollHeight;
});
var st = document.querySelector("[data-run-status]"); if (st) st.textContent = s.status;
if (s.live) setTimeout(tick, 1500); else setTimeout(function () { location.reload(); }, 800);
})
.catch(function () { setTimeout(tick, 5000); });
};
setTimeout(tick, 1500);
}
/* Keyboard: / focuses search; g c / g p / g i jump; ? shows the sheet. */
var pending = null;
document.addEventListener("keydown", function (e) {
if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
if (e.key === "/") { var s = document.querySelector("input[name=q]"); if (s) { e.preventDefault(); s.focus(); } return; }
if (e.key === "y" && !e.ctrlKey && !e.metaKey && !e.altKey && permalink()) { history.replaceState(null, "", permalink()); pending = null; return; }
if (e.key === "?") { var k = document.getElementById("keys"); if (k) k.hidden = !k.hidden; return; }
var repo = document.body.getAttribute("data-repo");
if (pending === "g" && repo) {
var to = { c: "", p: "/proposals", i: "/issues", h: "/changes/" + (document.body.getAttribute("data-ref") || "main"), b: "/bookmarks", t: "/trust" }[e.key];
if (to !== undefined) location.href = repo + to;
pending = null; return;
}
pending = e.key === "g" ? "g" : null;
});
// Live conflict resolution (hub spec §6.10): who else is resolving this
// conflict, their choices region by region (the last one wins), and the
// resolution when someone commits it.
var live = document.querySelector("[data-resolve-live]");
if (live && window.EventSource && window.fetch) {
var url = live.getAttribute("data-resolve-live"), csrf = live.getAttribute("data-csrf");
var client = Math.random().toString(36).slice(2) + Date.now().toString(36);
var present = document.querySelector("[data-live-present]"), landed = document.querySelector("[data-live-landed]");
var applying = false;
var fieldsOf = function (region) {
return region === "keep"
? { radios: live.querySelectorAll('input[name="keep"]'), text: null }
: { radios: live.querySelectorAll('input[name="r' + region + '"]'), text: live.querySelector('textarea[name="t' + region + '"]') };
};
var apply = function (region, c) {
var f = fieldsOf(region);
applying = true;
f.radios.forEach(function (r) { r.checked = r.value === c.choice; });
if (f.text && c.choice === "text") f.text.value = c.text;
applying = false;
var sec = f.radios.length ? f.radios[0].closest("section") : null;
if (sec) sec.setAttribute("title", "chosen by " + c.by);
};
var send = function (region, choice, text) {
fetch(url, { method: "POST", headers: { "content-type": "application/json" }, credentials: "same-origin",
body: JSON.stringify({ _csrf: csrf, client: client, region: region, choice: choice, text: text || "" }) });
};
var timers = {};
live.addEventListener("change", function (ev) {
var t = ev.target;
if (applying || !t.name || t.type !== "radio") return;
var region = t.name === "keep" ? "keep" : t.name.slice(1);
var ta = live.querySelector('textarea[name="t' + region + '"]');
send(region, t.value, t.value === "text" && ta ? ta.value : "");
});
live.addEventListener("input", function (ev) {
var t = ev.target;
if (applying || t.tagName !== "TEXTAREA") return;
var region = t.name.slice(1);
live.querySelectorAll('input[name="r' + region + '"]').forEach(function (r) { r.checked = r.value === "text"; });
clearTimeout(timers[region]);
timers[region] = setTimeout(function () { send(region, "text", t.value); }, 400);
});
var es = new EventSource(url + "?client=" + encodeURIComponent(client));
var showPresent = function (names) {
present.hidden = names.length < 2;
present.textContent = "Resolving together: " + names.join(", ") + ". Choices are shared as you make them; the last choice for a conflict wins.";
};
es.addEventListener("snapshot", function (e) {
var d = JSON.parse(e.data);
showPresent(d.present);
Object.keys(d.choices).forEach(function (r) { apply(r, d.choices[r]); });
if (d.keep) apply("keep", d.keep);
});
es.addEventListener("presence", function (e) { showPresent(JSON.parse(e.data).present); });
es.addEventListener("choice", function (e) {
var d = JSON.parse(e.data);
if (d.client !== client) apply(d.region, d.choice);
});
es.addEventListener("landed", function (e) {
var d = JSON.parse(e.data);
es.close();
landed.hidden = false;
landed.textContent = "";
landed.appendChild(document.createTextNode(d.by + " committed the resolution: "));
var a = document.createElement("a");
a.href = d.url;
a.textContent = "see the change";
landed.appendChild(a);
live.querySelectorAll("input, textarea, button").forEach(function (x) { x.disabled = true; });
});
}
// Swimlanes of a proposal's bookmark (hub spec §6.7): drag a change onto
// another to plan a squash or a rebase. The hub only writes the plan;
// the author's client carries it out and signs it.
var plan = document.querySelector("[data-plan]");
if (plan && window.fetch) {
var panel = document.querySelector("[data-plan-panel]"), from = null, moved = false;
plan.querySelectorAll("[data-change]").forEach(function (a) {
a.style.cursor = "grab";
a.addEventListener("pointerdown", function (ev) { from = a.getAttribute("data-change"); moved = false; ev.preventDefault(); });
a.addEventListener("pointermove", function () { if (from) moved = true; });
a.addEventListener("click", function (ev) { if (moved) ev.preventDefault(); });
});
document.addEventListener("pointerup", function (ev) {
var start = from;
from = null;
if (!start) return;
var el = document.elementFromPoint(ev.clientX, ev.clientY), onto = el && el.closest ? el.closest("[data-change]") : null;
if (!onto || onto.getAttribute("data-change") === start) return;
var body = new URLSearchParams({ _csrf: plan.getAttribute("data-csrf"), change: start, target: onto.getAttribute("data-change") });
fetch(plan.getAttribute("data-plan"), { method: "POST", credentials: "same-origin", body: body })
.then(function (r) { return r.json(); })
.then(function (d) {
panel.hidden = false;
panel.textContent = "";
if (d.error) { panel.textContent = d.error; return; }
d.options.forEach(function (o) {
var p = document.createElement("p");
p.appendChild(document.createTextNode(o.summary + " "));
var a = document.createElement("a");
a.href = o.vscode;
a.textContent = "Apply in VS Code";
p.appendChild(a);
var code = document.createElement("pre");
code.className = "snippet";
code.textContent = o.command;
panel.appendChild(p);
panel.appendChild(code);
});
});
});
}
/* Sidebar menus (oxygenhub#82): type to filter a menu's items; a click
outside an open menu closes it. */
document.querySelectorAll("[data-menu-filter]").forEach(function (input) {
var items = input.closest(".menu-pop").querySelectorAll(".menu-item");
input.addEventListener("input", function () {
var q = input.value.trim().toLowerCase();
items.forEach(function (it) { it.hidden = q !== "" && it.textContent.toLowerCase().indexOf(q) < 0; });
});
});
document.querySelectorAll("details.menu").forEach(function (d) {
d.addEventListener("toggle", function () {
var f = d.querySelector("[data-menu-filter]");
if (d.open && f) f.focus();
});
});
document.addEventListener("click", function (ev) {
document.querySelectorAll("details.menu[open]").forEach(function (d) { if (!d.contains(ev.target)) d.open = false; });
});
/* ··· menus: copy a link to a part of the page, or some text. */
function copied(btn, text) {
if (navigator.clipboard) navigator.clipboard.writeText(text).then(function () {
var was = btn.textContent; btn.textContent = "Copied"; setTimeout(function () { btn.textContent = was; }, 1200);
});
var d = btn.closest("details"); if (d) d.open = false;
}
document.querySelectorAll("[data-copy-link]").forEach(function (b) {
b.addEventListener("click", function () { copied(b, location.origin + location.pathname + location.search + b.getAttribute("data-copy-link")); });
});
document.querySelectorAll("[data-copy-text]").forEach(function (b) {
b.addEventListener("click", function () { copied(b, b.getAttribute("data-copy-text")); });
});
document.querySelectorAll('.menu-actions a[href^="#"]').forEach(function (a) {
a.addEventListener("click", function () { var d = a.closest("details"); if (d) d.open = false; });
});
/* Settings pages: a form posts and the page comes back from the top.
Remember which section's form was sent, and on the same page's next
load scroll back to it, bringing the notice along. */
document.addEventListener("submit", function (e) {
var sec = e.target.closest ? e.target.closest("section[id], form[id], details[id], div[id]") : null;
if (!sec || !sec.id) return;
try { sessionStorage.setItem("oxy-return", location.pathname + "#" + sec.id); } catch (err) { /* private mode */ }
});
try {
var back = sessionStorage.getItem("oxy-return");
if (back && back.indexOf(location.pathname + "#") === 0) {
sessionStorage.removeItem("oxy-return");
var target = document.getElementById(back.split("#")[1]);
if (target) {
var flash = document.querySelector("main > .note, main > .error, .sections > .note, .sections > .error");
if (flash && !target.contains(flash)) target.parentNode.insertBefore(flash, target);
target.scrollIntoView({ block: "start" });
}
}
} catch (err) { /* no storage: nothing to restore */ }
/* Working (#142): on a repository whose store is remote, a land or a
page can take seconds. A pressed button says what it is doing, with
the CLI's spinner, and ignores a second press; a navigation that takes
more than a moment spins the Oxygen mark's rings, in the header and
beside the pointer. A quick page shows none of it. */
var SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
var waitTimer = 0, spinTimer = 0, pointer = null, mark = null;
document.addEventListener("pointermove", function (e) {
pointer = e.pointerType === "mouse" ? { x: e.clientX, y: e.clientY } : null;
if (mark && pointer) place();
}, { passive: true });
function place() {
if (pointer) { mark.style.left = pointer.x + 14 + "px"; mark.style.top = pointer.y + 14 + "px"; mark.classList.remove("corner"); }
else mark.classList.add("corner");
}
function waiting() {
clearTimeout(waitTimer);
waitTimer = setTimeout(function () {
if (!mark) {
var logo = document.querySelector("header .brand svg");
mark = document.createElement("div");
mark.className = "oxy-wait-mark";
mark.setAttribute("role", "status");
mark.setAttribute("aria-label", "Working");
if (logo) mark.appendChild(logo.cloneNode(true));
document.body.appendChild(mark);
}
place();
document.documentElement.classList.add("oxy-wait");
}, 300);
}
function busy(button, form) {
if (!button) return;
var label = button.getAttribute("data-busy") || button.textContent.trim();
button.dataset.idle = button.innerHTML;
button.style.minWidth = button.offsetWidth + "px";
button.textContent = label + " ";
var glyph = document.createElement("span");
glyph.className = "oxy-spin";
glyph.setAttribute("aria-hidden", "true");
glyph.textContent = SPIN[0];
button.appendChild(glyph);
button.setAttribute("aria-busy", "true");
var i = 0;
clearInterval(spinTimer);
spinTimer = setInterval(function () { glyph.textContent = SPIN[++i % SPIN.length]; }, 80);
/* Disabled only after the form's data is gathered, so the button's
own name and value still go with it. */
setTimeout(function () { button.disabled = true; }, 0);
form.dataset.sending = "1";
}
document.addEventListener("submit", function (e) {
var form = e.target;
if (form.dataset.sending) { e.preventDefault(); return; }
if (e.defaultPrevented || (form.target && form.target !== "_self")) return;
if ((form.getAttribute("method") || "get").toLowerCase() === "post") {
busy(e.submitter || form.querySelector("button[type=submit], button:not([type])"), form);
}
waiting();
});
document.addEventListener("click", function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var a = e.target.closest ? e.target.closest("a[href]") : null;
if (!a || a.hasAttribute("download") || (a.target && a.target !== "_self")) return;
var url = new URL(a.href, location.href);
if (url.origin !== location.origin) return;
if (url.pathname === location.pathname && url.search === location.search && url.hash) return;
waiting();
});
/* Back to a page the browser kept: it is not working any more. */
window.addEventListener("pageshow", function (e) {
if (!e.persisted) return;
clearTimeout(waitTimer);
clearInterval(spinTimer);
document.documentElement.classList.remove("oxy-wait");
document.querySelectorAll("[aria-busy=true][data-idle]").forEach(function (b) {
b.innerHTML = b.dataset.idle;
b.disabled = false;
b.removeAttribute("aria-busy");
b.style.minWidth = "";
if (b.form) delete b.form.dataset.sending;
});
});
/* AI assistants (#109): a provider fills the name, endpoint and API;
List models asks the endpoint what it offers and fills the model
field's suggestions. Without this script, both are typed by hand. */
document.querySelectorAll("[data-preset-select]").forEach(function (sel) {
var form = sel.form;
function apply() {
var o = sel.options[sel.selectedIndex];
if (!o || !o.value) return;
var name = form.querySelector("[name=name]"), ep = form.querySelector("[name=endpoint]"), api = form.querySelector("[name=api]");
name.placeholder = o.dataset.name; ep.value = o.dataset.endpoint; api.value = o.dataset.api;
}
sel.addEventListener("change", apply);
apply();
});
document.querySelectorAll("[data-list-models]").forEach(function (b) {
b.addEventListener("click", function () {
var form = b.form, input = document.getElementById(b.dataset.for);
var list = document.getElementById(b.dataset.for + "-list"), note = document.querySelector("[data-models-note='" + b.dataset.for + "']");
var body = new URLSearchParams();
body.set("_csrf", b.dataset.csrf);
["endpoint", "api", "token", "preset"].forEach(function (k) { var el = form.querySelector("[name=" + k + "]"); if (el) body.set(k, el.value); });
if (b.dataset.id) body.set("id", b.dataset.id);
if (note) note.textContent = "Asking the provider…";
fetch(b.dataset.listModels, { method: "POST", body: body, credentials: "same-origin" })
.then(function (r) { return r.json(); })
.then(function (j) {
if (j.models) {
list.innerHTML = "";
j.models.forEach(function (m) { var o = document.createElement("option"); o.value = m; list.appendChild(o); });
if (note) note.textContent = j.models.length + " models: pick one in the Model field.";
input.focus();
} else if (note) {
note.textContent = "No list: " + (j.error || "unknown") + ". Type the model's name.";
}
})
.catch(function () { if (note) note.textContent = "The hub could not ask; type the model's name."; });
});
});
})();