91 lines (80 loc) · 3.03 KB
 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};