1import { $$ } from "./dom";
2
3// Copy button that writes `getText()` to the clipboard, with "Copied"/"Failed" feedback.
4const makeCopyBtn = (
5 label: string,
6 getText: () => string,
7 signal: AbortSignal,
8): HTMLButtonElement => {
9 const btn = document.createElement("button");
10 btn.type = "button";
11 btn.className = "copy-btn";
12 btn.textContent = "Copy";
13 btn.setAttribute("aria-label", label);
14
15 btn.addEventListener("click", async (e) => {
16 e.preventDefault();
17 try {
18 await navigator.clipboard.writeText(getText());
19 btn.textContent = "Copied";
20 } catch (_) {
21 btn.textContent = "Failed";
22 }
23 setTimeout(() => { btn.textContent = "Copy"; }, 1500);
24 }, { signal });
25
26 return btn;
27};
28
29/**
30 * Copy button on every code block. Injected here (not in the render hook) so it
31 * also appears on router-swapped pages; the guard skips blocks already wired.
32 */
33export const initCopy = (signal: AbortSignal): void => {
34 $$<HTMLElement>(".highlight, pre:not(.chroma)").forEach((block) => {
35 if (block.closest(".src-view__code")) return; // /src/ has its own text "copy"
36 if (block.parentElement?.classList.contains("code-wrap")) return;
37 const code = block.querySelector("code") || block;
38
39 // Wrap in a non-scrolling parent so the button stays pinned while the
40 // code scrolls horizontally.
41 const wrap = document.createElement("div");
42 wrap.className = "code-wrap";
43 block.replaceWith(wrap);
44 wrap.appendChild(block);
45
46 wrap.appendChild(makeCopyBtn("Copy code to clipboard", () => code.innerText.replace(/\n+$/, ""), signal));
47 });
48};
49
50const copyFeedURL = async (link: HTMLAnchorElement): Promise<boolean> => {
51 try {
52 await navigator.clipboard.writeText(link.href);
53 return true;
54 } catch (_) {
55 return false;
56 }
57};
58
59/**
60 * Clicking any RSS feed link copies its URL instead of navigating. JS-only:
61 * without it the link just opens the feed, which is still a fine fallback.
62 */
63export const initFeedCopy = (signal: AbortSignal): void => {
64 // Small header links (home/blog/photos): popup above the link.
65 $$<HTMLAnchorElement>(".section-header__rss").forEach((link) => {
66 if (link.dataset.copyWired) return;
67 link.dataset.copyWired = "true";
68
69 link.addEventListener("click", async (e) => {
70 e.preventDefault();
71 const ok = await copyFeedURL(link);
72 const tip = document.createElement("span");
73 tip.className = "copy-tooltip";
74 tip.textContent = ok ? "Copied!" : "Copy failed";
75 link.appendChild(tip);
76 requestAnimationFrame(() => tip.classList.add("is-visible"));
77 setTimeout(() => tip.remove(), 1200);
78 }, { signal });
79 });
80
81 // /rss/ page rows: swap the value text in place, right where the click happened.
82 $$<HTMLAnchorElement>(".rss-feed-list .contact-item__link").forEach((link) => {
83 const value = link.querySelector(".contact-item__value");
84 if (!value || link.dataset.copyWired) return;
85 link.dataset.copyWired = "true";
86 const original = value.textContent;
87
88 link.addEventListener("click", async (e) => {
89 e.preventDefault();
90 const ok = await copyFeedURL(link);
91 value.textContent = ok ? "Copied!" : "Copy failed";
92 setTimeout(() => { value.textContent = original; }, 1200);
93 }, { signal });
94 });
95};