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