samjeston / openhub Public
Sam Jeston Working vertical 9aa3c45 · History
141 lines· 4.8 KB ·JavaScript
Raw
1// Small progressive enhancements. Every page works without this script.
2(() => {
3 "use strict";
4
5 const copyText = async (text, button) => {
6 try {
7 await navigator.clipboard.writeText(text);
8 } catch {
9 const area = document.createElement("textarea");
10 area.value = text;
11 document.body.appendChild(area);
12 area.select();
13 document.execCommand("copy");
14 area.remove();
15 }
16 const original = button.innerHTML;
17 button.innerHTML =
18 '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
19 button.classList.add("copied");
20 setTimeout(() => {
21 button.innerHTML = original;
22 button.classList.remove("copied");
23 }, 1500);
24 };
25
26 document.addEventListener("click", (event) => {
27 const target = event.target.closest("[data-copy], [data-copy-text]");
28 if (target) {
29 event.preventDefault();
30 const text = target.dataset.copyText ?? document.querySelector(target.dataset.copy)?.value ?? "";
31 copyText(text, target);
32 return;
33 }
34
35 const toggle = event.target.closest("[data-toggle]");
36 if (toggle) {
37 const el = document.getElementById(toggle.dataset.toggle);
38 if (el) {
39 el.hidden = !el.hidden;
40 if (!el.hidden) el.querySelector("input, textarea")?.focus();
41 }
42 return;
43 }
44
45 const collapse = event.target.closest("[data-collapse]");
46 if (collapse) {
47 collapse.closest(".diff-file")?.classList.toggle("collapsed");
48 return;
49 }
50
51 const cloneTab = event.target.closest("[data-clone-tab]");
52 if (cloneTab) {
53 const panel = cloneTab.closest(".clone-panel");
54 panel.querySelectorAll("[data-clone-tab]").forEach((t) => t.classList.toggle("selected", t === cloneTab));
55 panel.querySelectorAll("[data-clone-pane]").forEach((p) => {
56 p.hidden = p.dataset.clonePane !== cloneTab.dataset.cloneTab;
57 });
58 return;
59 }
60
61 // Close open dropdowns when clicking elsewhere.
62 document.querySelectorAll("details.dropdown[open]").forEach((d) => {
63 if (!d.contains(event.target)) d.removeAttribute("open");
64 });
65 });
66
67 // Only one dropdown open at a time; focus the filter when opening.
68 document.querySelectorAll("details.dropdown").forEach((details) => {
69 details.addEventListener("toggle", () => {
70 if (!details.open) return;
71 document.querySelectorAll("details.dropdown[open]").forEach((other) => {
72 if (other !== details) other.removeAttribute("open");
73 });
74 details.querySelector("[data-filter]")?.focus();
75 });
76 });
77
78 document.addEventListener("keydown", (event) => {
79 if (event.key === "Escape") {
80 document.querySelectorAll("details.dropdown[open]").forEach((d) => {
81 d.removeAttribute("open");
82 d.querySelector("summary")?.focus();
83 });
84 }
85 });
86
87 document.addEventListener("input", (event) => {
88 const filter = event.target.closest("[data-filter]");
89 if (!filter) return;
90 const query = filter.value.trim().toLowerCase();
91 filter
92 .closest(".dropdown-menu")
93 .querySelectorAll("[data-filter-item]")
94 .forEach((item) => {
95 item.hidden = query !== "" && !item.textContent.toLowerCase().includes(query);
96 });
97 });
98
99 document.addEventListener("change", (event) => {
100 const select = event.target.closest("select[data-autosubmit]");
101 if (select) select.form.submit();
102 });
103
104 document.addEventListener("submit", (event) => {
105 const message = event.target.dataset.confirm;
106 if (message && !window.confirm(message)) event.preventDefault();
107 });
108
109 // Highlight #L10 or #L10-L20 in file views.
110 const highlightLines = () => {
111 document.querySelectorAll("tr.highlighted").forEach((row) => row.classList.remove("highlighted"));
112 const match = location.hash.match(/^#L(\d+)(?:-L(\d+))?$/);
113 if (!match) return;
114 const start = Number(match[1]);
115 const end = Number(match[2] || match[1]);
116 for (let n = Math.min(start, end); n <= Math.max(start, end); n++) {
117 document.getElementById(`L${n}`)?.classList.add("highlighted");
118 }
119 };
120
121 // Shift-click a line number to select a range.
122 let anchorLine = null;
123 document.addEventListener("click", (event) => {
124 const link = event.target.closest(".code-view .num a");
125 if (!link) return;
126 const line = Number(link.getAttribute("href").slice(2));
127 if (event.shiftKey && anchorLine) {
128 event.preventDefault();
129 const [a, b] = [Math.min(anchorLine, line), Math.max(anchorLine, line)];
130 history.replaceState(null, "", `#L${a}-L${b}`);
131 highlightLines();
132 } else {
133 anchorLine = line;
134 }
135 });
136
137 window.addEventListener("hashchange", highlightLines);
138 highlightLines();
139 const first = document.querySelector("tr.highlighted");
140 if (first) first.scrollIntoView({ block: "center" });
141})();