95 lines (84 loc) · 3.21 KB
 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};