kepr.js

2.8 KB · raw

1// Kepr's browser islands (§18). Every page is complete without this file; it only adds the2// styled timeline tooltip, copy buttons and line-range highlighting.34function timelineTips() {5	const dots = document.querySelectorAll(".timeline .dot");6	if (dots.length === 0) return;7	const tip = document.createElement("div");8	tip.className = "timeline-tip";9	tip.hidden = true;10	document.body.append(tip);11	for (const dot of dots) {12		// The SVG <title> is the no-JavaScript tooltip; lift it out so the two never show together.13		const title = dot.querySelector("title");14		const [id, message, meta] = (title?.textContent ?? "").split("\n");15		title?.remove();16		dot.setAttribute("aria-label", `${id} ${message}`);17		dot.addEventListener("pointerenter", (event) => {18			tip.replaceChildren(line("tip-id", id), line("tip-msg", message), line("tip-meta", meta));19			tip.hidden = false;20			const left = event.clientX + 14;21			tip.style.left = `${left + 340 > window.innerWidth ? Math.max(8, event.clientX - 354) : left}px`;22			tip.style.top = `${event.clientY - 10}px`;23		});24		dot.addEventListener("pointerleave", () => {25			tip.hidden = true;26		});27	}28}2930function line(className, text) {31	const div = document.createElement("div");32	div.className = className;33	div.textContent = text ?? "";34	return div;35}3637function copyButtons() {38	for (const button of document.querySelectorAll(".copy-btn[data-copy]")) {39		button.hidden = false;40		button.addEventListener("click", async () => {41			await navigator.clipboard.writeText(button.dataset.copy);42			button.textContent = "copied";43			button.classList.add("copied");44			setTimeout(() => {45				button.textContent = "copy";46				button.classList.remove("copied");47			}, 1400);48		});49	}50}5152// `#L42` works through CSS :target alone; a range `#L42-L50` needs marking line by line.53function lineRanges() {54	const code = document.querySelector(".code");55	if (!code) return;56	let anchor = null;57	const mark = () => {58		for (const marked of code.querySelectorAll(".line.hl")) marked.classList.remove("hl");59		const match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash);60		if (!match) return;61		const from = Number(match[1]);62		const to = Number(match[2] ?? match[1]);63		anchor = from;64		for (let n = Math.min(from, to); n <= Math.max(from, to); n++) {65			document.getElementById(`L${n}`)?.classList.add("hl");66		}67		document.getElementById(`L${Math.min(from, to)}`)?.scrollIntoView({ block: "center" });68	};69	code.addEventListener("click", (event) => {70		const link = event.target.closest(".ln");71		if (!link || !event.shiftKey || anchor === null) return;72		event.preventDefault();73		const n = Number(link.textContent);74		history.replaceState(null, "", `#L${Math.min(anchor, n)}-L${Math.max(anchor, n)}`);75		mark();76	});77	window.addEventListener("hashchange", mark);78	mark();79}8081timelineTips();82copyButtons();83lineRanges();