OXYGENHUB
◆ v0.4.2 oxygenhub / assets / hub.js
27.5 KiB · JavaScript · last changed Reports: cards with a kind icon and health, frames that fit and share the hub's look 24 days ago source blame history raw
d23464c0 Andy McCall ↶ 1
/* Progressive enhancement only: every page works without this file. */
2
(function () {
3
  "use strict";
4
  /* The band's active tab: the longest link that prefixes the path. */
5
  var p = location.pathname, best = null;
6
  document.querySelectorAll(".band a").forEach(function (a) {
7
    var h = a.getAttribute("href");
8
    if (h !== "/" && (p === h || p.indexOf(h + "/") === 0) && (!best || h.length > best.getAttribute("href").length)) best = a;
9
  });
10
  if (best) best.classList.add("on");
11
14530604 Andy McCall ↶ 12
  /* Off-the-shelf editors, loaded only where a page asks for them:
13
     CodeMirror 6 over textarea.code, EasyMDE over textarea.md. */
14
  function load(src, css) {
15
    return new Promise(function (ok) {
16
      if (css) { var l = document.createElement("link"); l.rel = "stylesheet"; l.href = css; document.head.appendChild(l); }
17
      var s = document.createElement("script"); s.src = src; s.onload = ok; document.head.appendChild(s);
18
    });
19
  }
20
  if (document.querySelector("textarea.code")) load("/static/vendor/editor.js");
21
  var mds = document.querySelectorAll("textarea.md");
22
  if (mds.length) load("/static/vendor/easymde.min.js", "/static/vendor/easymde.min.css").then(function () {
23
    var E = window.EasyMDE;
24
    function b(name, action, text, title) { return { name: name, action: action, text: text, title: title, className: "mde-btn" }; }
25
    var toolbar = [b("bold", E.toggleBold, "B", "Bold"), b("italic", E.toggleItalic, "I", "Italic"), b("heading", E.toggleHeadingSmaller, "H", "Heading"), "|",
26
      b("code", E.toggleCodeBlock, "</>", "Code"), b("quote", E.toggleBlockquote, "“", "Quote"), b("ul", E.toggleUnorderedList, "•", "List"), b("ol", E.toggleOrderedList, "1.", "Numbered list"), "|",
27
      b("link", E.drawLink, "link", "Link"), "|", b("preview", E.togglePreview, "preview", "Preview")];
4f37afcc Andy McCall ↶ 28
    mds.forEach(function (t) { t._mde = new E({ element: t, spellChecker: false, status: false, forceSync: true, autoDownloadFontAwesome: false, toolbar: toolbar }); });
29
    /* An editor shown later (a :target edit form) lays itself out again. */
30
    window.addEventListener("hashchange", function () { mds.forEach(function (t) { if (t._mde) setTimeout(function () { t._mde.codemirror.refresh(); }, 0); }); });
14530604 Andy McCall ↶ 31
  });
32
84751769 Andy McCall ↶ 33
  /* Mermaid diagrams (```mermaid blocks in Markdown): drawn by the hub's own
34
     copy of Mermaid, loaded only on pages that have one, in strict mode (no
35
     scripts or HTML from the diagram), in the page's theme. A diagram that
36
     does not parse stays as its source, with Mermaid's reason under it. */
37
  var diagrams = document.querySelectorAll("pre > code.language-mermaid");
38
  if (diagrams.length) load("/static/vendor/mermaid.min.js").then(function () {
39
    var m = window.mermaid, dark = document.documentElement.dataset.theme === "dark";
40
    m.initialize({ startOnLoad: false, securityLevel: "strict", suppressErrorRendering: true, theme: dark ? "dark" : "default" });
41
    diagrams.forEach(function (code, i) {
42
      var pre = code.parentNode, source = code.textContent;
43
      m.render("mermaid-" + i, source).then(function (r) {
44
        var fig = document.createElement("figure");
45
        fig.className = "mermaid-diagram";
46
        fig.innerHTML = r.svg;
47
        fig.setAttribute("aria-label", "Diagram");
48
        pre.replaceWith(fig);
49
      }).catch(function (e) {
50
        var note = document.createElement("p");
51
        note.className = "small warn";
52
        note.textContent = "This diagram could not be drawn: " + ((e && e.message) || e);
53
        pre.after(note);
54
      });
55
    });
56
  });
57
f3e5e2bc Andy McCall ↶ 58
  /* Report frames (#127): a report's page says how tall it is, and the
59
     frame grows to fit; the frame is told the theme, at start and on a
60
     change. Only messages from the frame itself are taken. */
61
  var frames = document.querySelectorAll("iframe.rep-frame");
62
  var themeToFrames = function () {
63
    frames.forEach(function (f) { if (f.contentWindow) f.contentWindow.postMessage({ oxygenhubTheme: document.documentElement.dataset.theme || "dark" }, "*"); });
64
  };
65
  if (frames.length) window.addEventListener("message", function (e) {
66
    frames.forEach(function (f) {
67
      if (e.source !== f.contentWindow) return;
68
      var r = e.data && e.data.oxygenhubReport;
69
      if (!r) return;
70
      if (r.ready) themeToFrames();
71
      if (typeof r.height === "number" && r.height > 0) {
72
        f.style.height = Math.min(Math.max(r.height, 120), 40000) + "px";
73
        f.classList.add("sized");
74
      }
75
    });
76
  });
77
c40c2d3e Andy McCall ↶ 78
  /* Theme switch in the header: dark, light, auto (follows the system); kept in the theme cookie. */
79
  var toggle = document.querySelector("[data-theme-toggle]");
80
  if (toggle) {
d98a93c1 Andy McCall ↶ 81
    var root = document.documentElement, order = ["light", "dark", "auto"], glyph = { dark: "◐", light: "☀", auto: "◑" };
c40c2d3e Andy McCall ↶ 82
    var show = function () {
d98a93c1 Andy McCall ↶ 83
      var p = root.dataset.themePref || "light";
c40c2d3e Andy McCall ↶ 84
      toggle.title = "Theme: " + p + " (click to change)";
85
      toggle.setAttribute("aria-label", toggle.title);
86
      toggle.firstElementChild.textContent = glyph[p];
87
    };
88
    show();
89
    toggle.addEventListener("click", function () {
d98a93c1 Andy McCall ↶ 90
      var p = order[(order.indexOf(root.dataset.themePref || "light") + 1) % order.length];
c40c2d3e Andy McCall ↶ 91
      document.cookie = "theme=" + p + "; path=/; max-age=31536000; samesite=lax";
92
      root.dataset.themePref = p;
93
      root.dataset.theme = p === "auto" ? (window.matchMedia && matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
94
      show();
f3e5e2bc Andy McCall ↶ 95
      themeToFrames();
c40c2d3e Andy McCall ↶ 96
    });
97
  }
98
9df855f2 Andy McCall ↶ 99
  /* Diffs: unified or split (remembered in the diff cookie), collapse all. */
100
  var diffs = document.querySelectorAll("table.code.diff");
101
  if (diffs.length) {
102
    var pane = function (tr, col, side) {
103
      var n = document.createElement("td"), t = document.createElement("td");
104
      n.className = "n"; t.className = "t" + (side === "right" ? " mid" : "");
105
      if (!tr) { n.className += " none"; t.className += " none"; return [n, t]; }
106
      var kind = tr.classList.contains("add") ? "add" : tr.classList.contains("del") ? "del" : "ctx";
107
      if (kind !== "ctx") { n.className += " " + kind; t.className += " " + kind; }
108
      n.textContent = tr.cells[col].textContent;
109
      t.innerHTML = tr.cells[3].innerHTML;
110
      // Line comments belong to the projected (right) side only.
111
      if (side === "left") t.querySelectorAll(".linecomment").forEach(function (d) { d.remove(); });
112
      return [n, t];
113
    };
114
    var split = function (table) {
115
      var s = document.createElement("table");
116
      s.className = "code diff split";
117
      s.innerHTML = '<colgroup><col class="n"><col><col class="n"><col></colgroup>';
118
      var rows = [].slice.call(table.rows), i = 0;
119
      var add = function (l, r) {
120
        var tr = s.insertRow();
121
        pane(l, 0, "left").concat(pane(r, 1, "right")).forEach(function (c) { tr.appendChild(c); });
122
      };
123
      while (i < rows.length) {
124
        var r = rows[i];
125
        if (r.classList.contains("del") || r.classList.contains("add")) {
126
          var dels = [], adds = [];
127
          while (i < rows.length && rows[i].classList.contains("del")) dels.push(rows[i++]);
128
          while (i < rows.length && rows[i].classList.contains("add")) adds.push(rows[i++]);
129
          for (var k = 0; k < Math.max(dels.length, adds.length); k++) add(dels[k], adds[k]);
130
          continue;
131
        }
132
        if (r.classList.contains("gap") || r.querySelector("td.thread")) {
133
          var tr = s.insertRow(), td = tr.insertCell();
134
          tr.className = r.className; td.colSpan = 4; td.className = r.cells[r.cells.length - 1].className;
135
          td.innerHTML = r.cells[r.cells.length - 1].innerHTML;
136
        } else {
137
          add(r, r);
138
        }
139
        i++;
140
      }
141
      return s;
142
    };
143
    var cookieMode = (document.cookie.match(/(?:^|; )diff=(split|unified)/) || [])[1] || "unified";
144
    var apply = function (mode) {
145
      diffs.forEach(function (t) {
146
        if (mode === "split" && !t._split) { t._split = split(t); t.parentNode.insertBefore(t._split, t.nextSibling); }
147
        t.hidden = mode === "split";
148
        if (t._split) t._split.hidden = mode !== "split";
149
      });
150
      document.querySelectorAll("[data-diff-mode]").forEach(function (a) { a.classList.toggle("on", a.getAttribute("data-diff-mode") === mode); });
151
    };
152
    document.querySelectorAll("[data-diff-modes], [data-diff-collapse]").forEach(function (e) { e.hidden = false; });
153
    document.querySelectorAll("[data-diff-mode]").forEach(function (a) {
154
      a.addEventListener("click", function (ev) {
155
        ev.preventDefault();
156
        var mode = a.getAttribute("data-diff-mode");
157
        document.cookie = "diff=" + mode + "; path=/; max-age=31536000; samesite=lax";
158
        apply(mode);
159
      });
160
    });
161
    document.querySelectorAll("[data-diff-collapse]").forEach(function (a) {
162
      a.addEventListener("click", function (ev) {
163
        ev.preventDefault();
164
        var files = document.querySelectorAll("details.file-diff"), anyOpen = [].some.call(files, function (d) { return d.open; });
165
        files.forEach(function (d) { d.open = !anyOpen; });
166
        a.textContent = anyOpen ? "expand all" : "collapse all";
167
      });
168
    });
169
    apply(cookieMode);
170
  }
171
64caed18 Andy McCall ↶ 172
  /* Settings pages: mark the section in view in the list of sections. */
173
  var navLinks = document.querySelectorAll(".section-nav a[href^='#']");
174
  if (navLinks.length && "IntersectionObserver" in window) {
175
    var byId = {};
176
    navLinks.forEach(function (a) { byId[a.getAttribute("href").slice(1)] = a; });
177
    var io = new IntersectionObserver(function (entries) {
178
      entries.forEach(function (e) {
179
        if (e.isIntersecting && byId[e.target.id]) {
180
          navLinks.forEach(function (a) { a.classList.remove("on"); });
181
          byId[e.target.id].classList.add("on");
182
        }
183
      });
184
    }, { rootMargin: "0px 0px -70% 0px" });
185
    Object.keys(byId).forEach(function (id) { var el = document.getElementById(id); if (el) io.observe(el); });
186
  }
187
d23464c0 Andy McCall ↶ 188
  /* Copy buttons. */
189
  document.querySelectorAll("[data-copy]").forEach(function (b) {
190
    b.addEventListener("click", function () {
191
      navigator.clipboard.writeText(b.getAttribute("data-copy")).then(function () {
192
        var t = b.textContent; b.textContent = "copied"; setTimeout(function () { b.textContent = t; }, 1200);
193
      });
194
    });
195
  });
196
197
  /* Tree rows: the last change per entry, fetched after the page paints. */
198
  var tree = document.querySelector("table.tree[data-last]");
199
  if (tree) {
200
    var paths = [].map.call(tree.querySelectorAll("tr[data-path]"), function (r) { return r.getAttribute("data-path"); });
201
    if (paths.length) {
202
      fetch(tree.getAttribute("data-last") + "?paths=" + encodeURIComponent(paths.join(",")), { credentials: "same-origin" })
203
        .then(function (r) { return r.json(); })
204
        .then(function (rows) {
205
          rows.forEach(function (c) {
206
            var tr = tree.querySelector('tr[data-path="' + CSS.escape(c.path) + '"]');
207
            if (!tr) return;
208
            var a = tr.querySelector("td.last a"), t = tr.querySelector("td.age");
209
            if (a) { a.textContent = c.title; a.href = tree.getAttribute("data-change") + c.hash; }
210
            if (t) { t.textContent = c.ago; t.title = c.when; }
211
          });
212
        }).catch(function () {});
213
    }
214
  }
215
216
  /* Line anchors: #L12 or #L12-L20 highlight a range and clicking a number selects. */
217
  function mark() {
218
    var m = location.hash.match(/^#L(\d+)(?:-L(\d+))?$/);
219
    document.querySelectorAll("tr.hl").forEach(function (r) { r.classList.remove("hl"); });
6ae41081 Andy McCall ↶ 220
    if (!m) { showBar(0, 0); return; }
d23464c0 Andy McCall ↶ 221
    var a = +m[1], b = +(m[2] || m[1]);
222
    for (var i = Math.min(a, b); i <= Math.max(a, b); i++) {
223
      var row = document.getElementById("L" + i);
224
      if (row) row.classList.add("hl");
225
    }
226
    var first = document.getElementById("L" + Math.min(a, b));
227
    if (first && !mark.done) { first.scrollIntoView({ block: "center" }); mark.done = true; }
6ae41081 Andy McCall ↶ 228
    showBar(Math.min(a, b), Math.max(a, b));
229
  }
230
  /* The bar for a selected range: copy its text, or a permalink to it. */
231
  var bar = document.querySelector(".linebar");
232
  function showBar(a, b) {
233
    if (!bar) return;
234
    bar.hidden = !a;
235
    if (!a) return;
236
    bar.querySelector("[data-linebar-label]").textContent = a === b ? "Line " + a : "Lines " + a + "–" + b;
237
    bar.setAttribute("data-range", a + "-" + b);
238
  }
239
  function flash(btn) { var t = btn.textContent; btn.textContent = "Copied"; setTimeout(function () { btn.textContent = t; }, 1200); }
240
  if (bar) {
241
    bar.querySelector("[data-copy-lines]").addEventListener("click", function (ev) {
242
      var r = bar.getAttribute("data-range").split("-"), out = [];
243
      for (var i = +r[0]; i <= +r[1]; i++) { var row = document.getElementById("L" + i); if (row) out.push(row.querySelector("td.t").textContent); }
244
      navigator.clipboard.writeText(out.join("\n") + "\n").then(function () { flash(ev.target); });
245
    });
246
    var pl = bar.querySelector("[data-copy-permalink]");
247
    if (pl) pl.addEventListener("click", function () {
248
      navigator.clipboard.writeText(new URL(document.body.getAttribute("data-permalink"), location.href).href + location.hash).then(function () { flash(pl); });
249
    });
d23464c0 Andy McCall ↶ 250
  }
6ae41081 Andy McCall ↶ 251
  function permalink() { var p = document.body.getAttribute("data-permalink"); return p ? p + location.hash : null; }
d23464c0 Andy McCall ↶ 252
  window.addEventListener("hashchange", mark);
253
  mark();
254
  document.querySelectorAll("table.code td.n").forEach(function (td) {
255
    td.addEventListener("click", function (e) {
256
      var n = td.parentNode.id;
257
      if (e.shiftKey && location.hash.match(/^#L(\d+)/)) location.hash = location.hash.replace(/^#(L\d+).*/, "$1-" + n);
258
      else location.hash = n;
259
    });
260
  });
261
42fa9554 Andy McCall ↶ 262
  /* Flow run page: statuses and new log lines while the run is live. */
263
  var liveUrl = document.body.getAttribute("data-flow-live");
264
  if (liveUrl) {
265
    var tick = function () {
266
      var after = [].map.call(document.querySelectorAll("details[data-job]"), function (d) {
267
        return d.getAttribute("data-job") + ":" + d.querySelector("[data-log]").getAttribute("data-seq");
268
      }).join(",");
269
      fetch(liveUrl + "?after=" + after, { headers: { accept: "application/json" } })
270
        .then(function (r) { return r.json(); })
271
        .then(function (s) {
272
          s.jobs.forEach(function (j) {
273
            var cls = "st-" + j.status;
274
            var node = document.querySelector("[data-job-node='" + j.id + "']");
275
            if (node) {
276
              node.setAttribute("class", "node " + cls);
277
              node.querySelector("[data-glyph]").textContent = j.glyph;
278
              node.querySelector("[data-meta]").textContent = j.status + " · " + j.duration;
279
            }
280
            var d = document.querySelector("details[data-job='" + j.id + "']");
281
            if (!d) return;
282
            d.className = "panel flow-job " + cls;
283
            var g = d.querySelector("summary [data-glyph]");
284
            g.className = "flow-glyph " + cls; g.textContent = j.glyph;
285
            d.querySelector("summary [data-meta]").textContent = j.status + (j.exit_code !== null ? " · exit " + j.exit_code : "") + " · " + j.duration;
286
            if (j.status === "running" || j.status === "failed") d.open = true;
287
            var log = d.querySelector("[data-log]");
288
            var stick = log.scrollTop + log.clientHeight >= log.scrollHeight - 4;
289
            j.lines.forEach(function (l) {
290
              var ln = document.createElement("span"); ln.className = "ln s-" + l.stream;
291
              var t = document.createElement("span"); t.className = "t"; t.textContent = (l.t / 1000).toFixed(1) + "s";
292
              ln.appendChild(t); ln.appendChild(document.createTextNode(l.text));
293
              log.appendChild(ln); log.appendChild(document.createTextNode("\n"));
294
              log.setAttribute("data-seq", l.seq);
295
            });
296
            if (stick && j.lines.length) log.scrollTop = log.scrollHeight;
297
          });
298
          var st = document.querySelector("[data-run-status]"); if (st) st.textContent = s.status;
299
          if (s.live) setTimeout(tick, 1500); else setTimeout(function () { location.reload(); }, 800);
300
        })
301
        .catch(function () { setTimeout(tick, 5000); });
302
    };
303
    setTimeout(tick, 1500);
304
  }
305
d23464c0 Andy McCall ↶ 306
  /* Keyboard: / focuses search; g c / g p / g i jump; ? shows the sheet. */
307
  var pending = null;
308
  document.addEventListener("keydown", function (e) {
309
    if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
310
    if (e.key === "/") { var s = document.querySelector("input[name=q]"); if (s) { e.preventDefault(); s.focus(); } return; }
6ae41081 Andy McCall ↶ 311
    if (e.key === "y" && !e.ctrlKey && !e.metaKey && !e.altKey && permalink()) { history.replaceState(null, "", permalink()); pending = null; return; }
d23464c0 Andy McCall ↶ 312
    if (e.key === "?") { var k = document.getElementById("keys"); if (k) k.hidden = !k.hidden; return; }
313
    var repo = document.body.getAttribute("data-repo");
314
    if (pending === "g" && repo) {
315
      var to = { c: "", p: "/proposals", i: "/issues", h: "/changes/" + (document.body.getAttribute("data-ref") || "main"), b: "/bookmarks", t: "/trust" }[e.key];
316
      if (to !== undefined) location.href = repo + to;
317
      pending = null; return;
318
    }
319
    pending = e.key === "g" ? "g" : null;
320
  });
5e6d6484 Andy McCall ↶ 321
322
  // Live conflict resolution (hub spec §6.10): who else is resolving this
323
  // conflict, their choices region by region (the last one wins), and the
324
  // resolution when someone commits it.
325
  var live = document.querySelector("[data-resolve-live]");
326
  if (live && window.EventSource && window.fetch) {
327
    var url = live.getAttribute("data-resolve-live"), csrf = live.getAttribute("data-csrf");
328
    var client = Math.random().toString(36).slice(2) + Date.now().toString(36);
329
    var present = document.querySelector("[data-live-present]"), landed = document.querySelector("[data-live-landed]");
330
    var applying = false;
331
    var fieldsOf = function (region) {
332
      return region === "keep"
333
        ? { radios: live.querySelectorAll('input[name="keep"]'), text: null }
334
        : { radios: live.querySelectorAll('input[name="r' + region + '"]'), text: live.querySelector('textarea[name="t' + region + '"]') };
335
    };
336
    var apply = function (region, c) {
337
      var f = fieldsOf(region);
338
      applying = true;
339
      f.radios.forEach(function (r) { r.checked = r.value === c.choice; });
340
      if (f.text && c.choice === "text") f.text.value = c.text;
341
      applying = false;
342
      var sec = f.radios.length ? f.radios[0].closest("section") : null;
343
      if (sec) sec.setAttribute("title", "chosen by " + c.by);
344
    };
345
    var send = function (region, choice, text) {
346
      fetch(url, { method: "POST", headers: { "content-type": "application/json" }, credentials: "same-origin",
347
        body: JSON.stringify({ _csrf: csrf, client: client, region: region, choice: choice, text: text || "" }) });
348
    };
349
    var timers = {};
350
    live.addEventListener("change", function (ev) {
351
      var t = ev.target;
352
      if (applying || !t.name || t.type !== "radio") return;
353
      var region = t.name === "keep" ? "keep" : t.name.slice(1);
354
      var ta = live.querySelector('textarea[name="t' + region + '"]');
355
      send(region, t.value, t.value === "text" && ta ? ta.value : "");
356
    });
357
    live.addEventListener("input", function (ev) {
358
      var t = ev.target;
359
      if (applying || t.tagName !== "TEXTAREA") return;
360
      var region = t.name.slice(1);
361
      live.querySelectorAll('input[name="r' + region + '"]').forEach(function (r) { r.checked = r.value === "text"; });
362
      clearTimeout(timers[region]);
363
      timers[region] = setTimeout(function () { send(region, "text", t.value); }, 400);
364
    });
365
    var es = new EventSource(url + "?client=" + encodeURIComponent(client));
366
    var showPresent = function (names) {
367
      present.hidden = names.length < 2;
368
      present.textContent = "Resolving together: " + names.join(", ") + ". Choices are shared as you make them; the last choice for a conflict wins.";
369
    };
370
    es.addEventListener("snapshot", function (e) {
371
      var d = JSON.parse(e.data);
372
      showPresent(d.present);
373
      Object.keys(d.choices).forEach(function (r) { apply(r, d.choices[r]); });
374
      if (d.keep) apply("keep", d.keep);
375
    });
376
    es.addEventListener("presence", function (e) { showPresent(JSON.parse(e.data).present); });
377
    es.addEventListener("choice", function (e) {
378
      var d = JSON.parse(e.data);
379
      if (d.client !== client) apply(d.region, d.choice);
380
    });
381
    es.addEventListener("landed", function (e) {
382
      var d = JSON.parse(e.data);
383
      es.close();
384
      landed.hidden = false;
385
      landed.textContent = "";
386
      landed.appendChild(document.createTextNode(d.by + " committed the resolution: "));
387
      var a = document.createElement("a");
388
      a.href = d.url;
389
      a.textContent = "see the change";
390
      landed.appendChild(a);
391
      live.querySelectorAll("input, textarea, button").forEach(function (x) { x.disabled = true; });
392
    });
393
  }
2bfe5ef6 Andy McCall ↶ 394
395
  // Swimlanes of a proposal's bookmark (hub spec §6.7): drag a change onto
396
  // another to plan a squash or a rebase. The hub only writes the plan;
397
  // the author's client carries it out and signs it.
398
  var plan = document.querySelector("[data-plan]");
399
  if (plan && window.fetch) {
400
    var panel = document.querySelector("[data-plan-panel]"), from = null, moved = false;
401
    plan.querySelectorAll("[data-change]").forEach(function (a) {
402
      a.style.cursor = "grab";
403
      a.addEventListener("pointerdown", function (ev) { from = a.getAttribute("data-change"); moved = false; ev.preventDefault(); });
404
      a.addEventListener("pointermove", function () { if (from) moved = true; });
405
      a.addEventListener("click", function (ev) { if (moved) ev.preventDefault(); });
406
    });
407
    document.addEventListener("pointerup", function (ev) {
408
      var start = from;
409
      from = null;
410
      if (!start) return;
411
      var el = document.elementFromPoint(ev.clientX, ev.clientY), onto = el && el.closest ? el.closest("[data-change]") : null;
412
      if (!onto || onto.getAttribute("data-change") === start) return;
413
      var body = new URLSearchParams({ _csrf: plan.getAttribute("data-csrf"), change: start, target: onto.getAttribute("data-change") });
414
      fetch(plan.getAttribute("data-plan"), { method: "POST", credentials: "same-origin", body: body })
415
        .then(function (r) { return r.json(); })
416
        .then(function (d) {
417
          panel.hidden = false;
418
          panel.textContent = "";
419
          if (d.error) { panel.textContent = d.error; return; }
420
          d.options.forEach(function (o) {
421
            var p = document.createElement("p");
422
            p.appendChild(document.createTextNode(o.summary + " "));
423
            var a = document.createElement("a");
424
            a.href = o.vscode;
425
            a.textContent = "Apply in VS Code";
426
            p.appendChild(a);
427
            var code = document.createElement("pre");
428
            code.className = "snippet";
429
            code.textContent = o.command;
430
            panel.appendChild(p);
431
            panel.appendChild(code);
432
          });
433
        });
434
    });
435
  }
cb71e18e Andy McCall ↶ 436
437
  /* Sidebar menus (oxygenhub#82): type to filter a menu's items; a click
438
     outside an open menu closes it. */
439
  document.querySelectorAll("[data-menu-filter]").forEach(function (input) {
440
    var items = input.closest(".menu-pop").querySelectorAll(".menu-item");
441
    input.addEventListener("input", function () {
442
      var q = input.value.trim().toLowerCase();
443
      items.forEach(function (it) { it.hidden = q !== "" && it.textContent.toLowerCase().indexOf(q) < 0; });
444
    });
445
  });
446
  document.querySelectorAll("details.menu").forEach(function (d) {
447
    d.addEventListener("toggle", function () {
448
      var f = d.querySelector("[data-menu-filter]");
449
      if (d.open && f) f.focus();
450
    });
451
  });
452
  document.addEventListener("click", function (ev) {
453
    document.querySelectorAll("details.menu[open]").forEach(function (d) { if (!d.contains(ev.target)) d.open = false; });
454
  });
4f37afcc Andy McCall ↶ 455
456
  /* ··· menus: copy a link to a part of the page, or some text. */
457
  function copied(btn, text) {
458
    if (navigator.clipboard) navigator.clipboard.writeText(text).then(function () {
459
      var was = btn.textContent; btn.textContent = "Copied"; setTimeout(function () { btn.textContent = was; }, 1200);
460
    });
461
    var d = btn.closest("details"); if (d) d.open = false;
462
  }
463
  document.querySelectorAll("[data-copy-link]").forEach(function (b) {
464
    b.addEventListener("click", function () { copied(b, location.origin + location.pathname + location.search + b.getAttribute("data-copy-link")); });
465
  });
466
  document.querySelectorAll("[data-copy-text]").forEach(function (b) {
467
    b.addEventListener("click", function () { copied(b, b.getAttribute("data-copy-text")); });
468
  });
469
  document.querySelectorAll('.menu-actions a[href^="#"]').forEach(function (a) {
470
    a.addEventListener("click", function () { var d = a.closest("details"); if (d) d.open = false; });
471
  });
ed89fae5 Andy McCall ↶ 472
473
  /* Settings pages: a form posts and the page comes back from the top.
474
     Remember which section's form was sent, and on the same page's next
475
     load scroll back to it, bringing the notice along. */
476
  document.addEventListener("submit", function (e) {
477
    var sec = e.target.closest ? e.target.closest("section[id], form[id], details[id], div[id]") : null;
478
    if (!sec || !sec.id) return;
479
    try { sessionStorage.setItem("oxy-return", location.pathname + "#" + sec.id); } catch (err) { /* private mode */ }
480
  });
481
  try {
482
    var back = sessionStorage.getItem("oxy-return");
483
    if (back && back.indexOf(location.pathname + "#") === 0) {
484
      sessionStorage.removeItem("oxy-return");
485
      var target = document.getElementById(back.split("#")[1]);
486
      if (target) {
487
        var flash = document.querySelector("main > .note, main > .error, .sections > .note, .sections > .error");
488
        if (flash && !target.contains(flash)) target.parentNode.insertBefore(flash, target);
489
        target.scrollIntoView({ block: "start" });
490
      }
491
    }
492
  } catch (err) { /* no storage: nothing to restore */ }
155cab14 Andy McCall ↶ 493
494
  /* AI assistants (#109): a provider fills the name, endpoint and API;
495
     List models asks the endpoint what it offers and fills the model
496
     field's suggestions. Without this script, both are typed by hand. */
497
  document.querySelectorAll("[data-preset-select]").forEach(function (sel) {
498
    var form = sel.form;
499
    function apply() {
500
      var o = sel.options[sel.selectedIndex];
501
      if (!o || !o.value) return;
502
      var name = form.querySelector("[name=name]"), ep = form.querySelector("[name=endpoint]"), api = form.querySelector("[name=api]");
503
      name.placeholder = o.dataset.name; ep.value = o.dataset.endpoint; api.value = o.dataset.api;
504
    }
505
    sel.addEventListener("change", apply);
506
    apply();
507
  });
508
  document.querySelectorAll("[data-list-models]").forEach(function (b) {
509
    b.addEventListener("click", function () {
510
      var form = b.form, input = document.getElementById(b.dataset.for);
511
      var list = document.getElementById(b.dataset.for + "-list"), note = document.querySelector("[data-models-note='" + b.dataset.for + "']");
512
      var body = new URLSearchParams();
513
      body.set("_csrf", b.dataset.csrf);
514
      ["endpoint", "api", "token", "preset"].forEach(function (k) { var el = form.querySelector("[name=" + k + "]"); if (el) body.set(k, el.value); });
515
      if (b.dataset.id) body.set("id", b.dataset.id);
516
      if (note) note.textContent = "Asking the provider…";
517
      fetch(b.dataset.listModels, { method: "POST", body: body, credentials: "same-origin" })
518
        .then(function (r) { return r.json(); })
519
        .then(function (j) {
520
          if (j.models) {
521
            list.innerHTML = "";
522
            j.models.forEach(function (m) { var o = document.createElement("option"); o.value = m; list.appendChild(o); });
523
            if (note) note.textContent = j.models.length + " models: pick one in the Model field.";
524
            input.focus();
525
          } else if (note) {
526
            note.textContent = "No list: " + (j.error || "unknown") + ". Type the model's name.";
527
          }
528
        })
529
        .catch(function () { if (note) note.textContent = "The hub could not ask; type the model's name."; });
530
    });
531
  });
d23464c0 Andy McCall ↶ 532
})();