From 461dc8ff536bb9fa2b719da71bb7be942a739fc8 Mon Sep 17 00:00:00 2001 From: Aditya Date: Tue, 4 Nov 2025 11:14:25 +0530 Subject: [PATCH] feat: add copy-to-clipboard button for code blocks Inject a top-right copy button into pre inside .nextra-code-block elements that copies code via the Clipboard API. Shows a success tick for 2s. --- pages/_app.js | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/pages/_app.js b/pages/_app.js index c233976..78de167 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -19,9 +19,139 @@ const App = ({ Component, pageProps }) => { script2.defer = true; document.body.appendChild(script2); + const COPY_SUCCESS_TIMEOUT = 2000; + + const copySvg = ` + + `; + + const tickSvg = ` + + + fill="#B4B4B8"/> + `; + + function createCopyButtonFor(block) { + try { + if (!block || block.dataset.copyAttached === "true") return; + const pre = block.querySelector("pre"); + const code = + block.querySelector("pre > code") || block.querySelector("code"); + if (!pre || !code) return; + + if (!pre.style.position || pre.style.position === "") + pre.style.position = "relative"; + + if (pre.querySelector("button[data-copy-btn]")) { + block.dataset.copyAttached = "true"; + return; + } + + const btn = document.createElement("button"); + btn.setAttribute("type", "button"); + btn.setAttribute("aria-label", "Copy Code Snippet"); + btn.setAttribute("title", "Copy Code Snippet"); + btn.dataset.copyBtn = "true"; + btn.innerHTML = copySvg; + + btn.style.position = "absolute"; + btn.style.right = "7px"; + btn.style.top = "10px"; + btn.style.zIndex = "1"; + btn.style.border = "1px solid rgba(0,0,0,0.06)"; + btn.style.padding = "8px"; + btn.style.backdropFilter = "blur(8px)"; + btn.style.cursor = "pointer"; + btn.style.fontSize = "14px"; + btn.style.borderRadius = "10px"; + btn.style.boxShadow = "inset 0 0 0 1.2px rgba(0,0,0,0.04)"; + btn.style.background = "rgba(255,255,255,0.6)"; + btn.style.display = "flex"; + btn.style.alignItems = "center"; + btn.style.justifyContent = "center"; + + let timeoutId = null; + + async function doCopy() { + const text = code.innerText || code.textContent || ""; + if (!text) return; + let ok = false; + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + ok = true; + } else { + const ta = document.createElement("textarea"); + ta.value = text; + document.body.appendChild(ta); + ta.select(); + ok = document.execCommand("copy"); + document.body.removeChild(ta); + } + } catch (e) { + console.warn("copy failed", e); + } + + if (ok) { + btn.innerHTML = tickSvg; + btn.setAttribute("aria-label", "Copied"); + btn.setAttribute("title", "Copied"); + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + btn.innerHTML = copySvg; + btn.setAttribute("aria-label", "Copy Code Snippet"); + btn.setAttribute("title", "Copy Code Snippet"); + }, COPY_SUCCESS_TIMEOUT); + } else { + btn.animate( + [ + { transform: "translateX(0)" }, + { transform: "translateX(-4px)" }, + { transform: "translateX(4px)" }, + { transform: "translateX(0)" }, + ], + { duration: 300 } + ); + } + } + + btn.addEventListener("click", doCopy); + btn.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + doCopy(); + } + }); + + pre.appendChild(btn); + block.dataset.copyAttached = "true"; + } catch (err) { + console.error("Error attaching copy button", err); + } + } + + function attachToAll() { + document + .querySelectorAll(".nextra-code-block") + .forEach(createCopyButtonFor); + } + + attachToAll(); + + const mo = new MutationObserver((mutations) => { + for (const m of mutations) { + if (m.type === "childList" && m.addedNodes.length) { + attachToAll(); + break; + } + } + }); + mo.observe(document.body, { childList: true, subtree: true }); + return () => { document.body.removeChild(script1); document.body.removeChild(script2); + mo.disconnect(); }; }, []);