// 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();