| 1 | // Progressive enhancements. Everything works without this file. |
| 2 | (function () { |
| 3 | "use strict"; |
| 4 | |
| 5 | // Index: instant filter; "/" focuses it, Escape clears it. |
| 6 | var input = document.querySelector("[data-filter]"); |
| 7 | if (input) { |
| 8 | var rows = Array.prototype.slice.call(document.querySelectorAll("[data-search]")); |
| 9 | var sections = Array.prototype.slice.call(document.querySelectorAll("[data-section]")); |
| 10 | var empty = document.querySelector("[data-filter-empty]"); |
| 11 | |
| 12 | var apply = function () { |
| 13 | var terms = input.value.toLowerCase().split(/\s+/).filter(Boolean); |
| 14 | var shown = 0; |
| 15 | rows.forEach(function (row) { |
| 16 | var text = row.getAttribute("data-search"); |
| 17 | var match = terms.every(function (t) { return text.indexOf(t) !== -1; }); |
| 18 | row.hidden = !match; |
| 19 | if (match) shown++; |
| 20 | }); |
| 21 | sections.forEach(function (section) { |
| 22 | section.hidden = !section.querySelector("[data-search]:not([hidden])"); |
| 23 | }); |
| 24 | if (empty) empty.hidden = shown > 0; |
| 25 | }; |
| 26 | |
| 27 | input.addEventListener("input", apply); |
| 28 | input.addEventListener("keydown", function (e) { |
| 29 | if (e.key === "Escape") { |
| 30 | input.value = ""; |
| 31 | apply(); |
| 32 | input.blur(); |
| 33 | } |
| 34 | }); |
| 35 | document.addEventListener("keydown", function (e) { |
| 36 | if (e.key !== "/" || e.ctrlKey || e.metaKey || e.altKey) return; |
| 37 | var t = e.target; |
| 38 | if (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)) return; |
| 39 | e.preventDefault(); |
| 40 | input.focus(); |
| 41 | input.select(); |
| 42 | }); |
| 43 | } |
| 44 | // Blob: line ranges. #L12 works without JS (:target); here shift-click |
| 45 | // extends to #L12-L20 and the range is highlighted on load. |
| 46 | var code = document.querySelector("[data-lines]"); |
| 47 | if (code) { |
| 48 | var anchor = null; |
| 49 | var select = function (from, to) { |
| 50 | Array.prototype.forEach.call(code.querySelectorAll("tr.selected"), function (tr) { |
| 51 | tr.classList.remove("selected"); |
| 52 | }); |
| 53 | if (!from) return; |
| 54 | if (to < from) { var t = from; from = to; to = t; } |
| 55 | for (var n = from; n <= to; n++) { |
| 56 | var row = document.getElementById("L" + n); |
| 57 | if (row) row.classList.add("selected"); |
| 58 | } |
| 59 | }; |
| 60 | var fromHash = function () { |
| 61 | var m = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash); |
| 62 | if (!m) return select(0); |
| 63 | anchor = +m[1]; |
| 64 | select(anchor, +(m[2] || m[1])); |
| 65 | if (m[2]) { |
| 66 | var row = document.getElementById("L" + Math.min(+m[1], +m[2])); |
| 67 | if (row) row.scrollIntoView({ block: "center" }); |
| 68 | } |
| 69 | }; |
| 70 | code.addEventListener("click", function (e) { |
| 71 | var a = e.target.closest && e.target.closest(".ln a"); |
| 72 | if (!a) return; |
| 73 | var n = +a.textContent; |
| 74 | e.preventDefault(); |
| 75 | var hash = "#L" + n; |
| 76 | if (e.shiftKey && anchor && anchor !== n) { |
| 77 | hash = "#L" + Math.min(anchor, n) + "-L" + Math.max(anchor, n); |
| 78 | } else { |
| 79 | anchor = n; |
| 80 | } |
| 81 | history.replaceState(null, "", hash); |
| 82 | fromHash(); |
| 83 | }); |
| 84 | window.addEventListener("hashchange", fromHash); |
| 85 | fromHash(); |
| 86 | } |
| 87 | |
| 88 | // Summary: clone protocol switch and copy button. |
| 89 | var clone = document.querySelector("[data-clone]"); |
| 90 | if (clone) { |
| 91 | var KEY = "ferrenium-clone"; |
| 92 | var status = clone.querySelector("[data-copy-status]"); |
| 93 | var choose = function (i) { |
| 94 | var rows = clone.querySelectorAll("[data-clone-url]"); |
| 95 | if (!rows[i]) i = 0; |
| 96 | Array.prototype.forEach.call(rows, function (row, j) { row.hidden = j !== i; }); |
| 97 | Array.prototype.forEach.call(clone.querySelectorAll("[data-clone-choice]"), function (b, j) { |
| 98 | b.setAttribute("aria-pressed", j === i ? "true" : "false"); |
| 99 | }); |
| 100 | }; |
| 101 | var saved = 0; |
| 102 | try { saved = +(localStorage.getItem(KEY) || 0); } catch (e) {} |
| 103 | choose(saved); |
| 104 | clone.addEventListener("click", function (e) { |
| 105 | var choice = e.target.closest("[data-clone-choice]"); |
| 106 | if (choice) { |
| 107 | var i = +choice.getAttribute("data-clone-choice"); |
| 108 | choose(i); |
| 109 | try { localStorage.setItem(KEY, i); } catch (err) {} |
| 110 | return; |
| 111 | } |
| 112 | var copy = e.target.closest("[data-copy]"); |
| 113 | if (!copy) return; |
| 114 | var input = copy.parentNode.querySelector("input"); |
| 115 | var done = function (ok) { |
| 116 | status.textContent = ok ? "[Copied]" : "[Error: copy failed]"; |
| 117 | status.classList.toggle("status-error", !ok); |
| 118 | clearTimeout(done.t); |
| 119 | done.t = setTimeout(function () { status.textContent = ""; }, 2000); |
| 120 | }; |
| 121 | if (navigator.clipboard) { |
| 122 | navigator.clipboard.writeText(input.value).then(function () { done(true); }, function () { done(false); }); |
| 123 | } else { |
| 124 | input.select(); |
| 125 | done(document.execCommand("copy")); |
| 126 | } |
| 127 | }); |
| 128 | } |
| 129 | })(); |