// Kepr's browser islands (ยง18). Every page is complete without this file; it only adds the // styled timeline tooltip, copy buttons and line-range highlighting. function timelineTips() { const dots = document.querySelectorAll(".timeline .dot"); if (dots.length === 0) return; const tip = document.createElement("div"); tip.className = "timeline-tip"; tip.hidden = true; document.body.append(tip); for (const dot of dots) { // The SVG is the no-JavaScript tooltip; lift it out so the two never show together. const title = dot.querySelector("title"); const [id, message, meta] = (title?.textContent ?? "").split("\n"); title?.remove(); dot.setAttribute("aria-label", `${id} ${message}`); dot.addEventListener("pointerenter", (event) => { tip.replaceChildren(line("tip-id", id), line("tip-msg", message), line("tip-meta", meta)); tip.hidden = false; const left = event.clientX + 14; tip.style.left = `${left + 340 > window.innerWidth ? Math.max(8, event.clientX - 354) : left}px`; tip.style.top = `${event.clientY - 10}px`; }); dot.addEventListener("pointerleave", () => { tip.hidden = true; }); } } function line(className, text) { const div = document.createElement("div"); div.className = className; div.textContent = text ?? ""; return div; } function copyButtons() { for (const button of document.querySelectorAll(".copy-btn[data-copy]")) { button.hidden = false; button.addEventListener("click", async () => { await navigator.clipboard.writeText(button.dataset.copy); button.textContent = "copied"; button.classList.add("copied"); setTimeout(() => { button.textContent = "copy"; button.classList.remove("copied"); }, 1400); }); } } // `#L42` works through CSS :target alone; a range `#L42-L50` needs marking line by line. function lineRanges() { const code = document.querySelector(".code"); if (!code) return; let anchor = null; const mark = () => { for (const marked of code.querySelectorAll(".line.hl")) marked.classList.remove("hl"); const match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash); if (!match) return; const from = Number(match[1]); const to = Number(match[2] ?? match[1]); anchor = from; for (let n = Math.min(from, to); n <= Math.max(from, to); n++) { document.getElementById(`L${n}`)?.classList.add("hl"); } document.getElementById(`L${Math.min(from, to)}`)?.scrollIntoView({ block: "center" }); }; code.addEventListener("click", (event) => { const link = event.target.closest(".ln"); if (!link || !event.shiftKey || anchor === null) return; event.preventDefault(); const n = Number(link.textContent); history.replaceState(null, "", `#L${Math.min(anchor, n)}-L${Math.max(anchor, n)}`); mark(); }); window.addEventListener("hashchange", mark); mark(); } timelineTips(); copyButtons(); lineRanges();