Skip to content

Commit

Permalink
Add copy button for code snippets (#126)
Browse files Browse the repository at this point in the history
* Add copy button for code snippets

* Updated copy button to use the hook from remix docs

* Apply suggestions from code review

---------

Co-authored-by: Brooks Lybrand <brookslybrand@gmail.com>
  • Loading branch information
sardorml and brookslybrand authored Oct 29, 2024
1 parent b758ab4 commit c4a4640
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 1 deletion.
10 changes: 9 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
import { isHost } from "./modules/http-utils/is-host";
import iconsHref from "~/icons.svg";
import stylesheet from "~/styles/tailwind.css?url";
import { useRef } from "react";
import { useCodeBlockCopyButton } from "./ui/utils";

export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
Expand All @@ -46,6 +48,9 @@ export function headers(_: HeadersArgs) {
export default function App() {
let colorScheme = useColorScheme();

let docsContainer = useRef<HTMLBodyElement>(null);
useCodeBlockCopyButton(docsContainer);

return (
<html
lang="en"
Expand All @@ -71,7 +76,10 @@ export default function App() {
<Meta />
<Links />
</head>
<body className="bg-white text-black antialiased selection:bg-blue-200 selection:text-black dark:bg-gray-900 dark:text-white dark:selection:bg-blue-800 dark:selection:text-white">
<body
ref={docsContainer}
className="bg-white text-black antialiased selection:bg-blue-200 selection:text-black dark:bg-gray-900 dark:text-white dark:selection:bg-blue-800 dark:selection:text-white"
>
<img
src={iconsHref}
alt=""
Expand Down
62 changes: 62 additions & 0 deletions app/ui/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useLocation } from "@remix-run/react";
import { useEffect, useState } from "react";

let hydrating = true;
Expand All @@ -10,3 +11,64 @@ export function useHydrated() {
}, []);
return hydrated;
}

export function useCodeBlockCopyButton(ref: React.RefObject<HTMLBodyElement>) {
let location = useLocation();
useEffect(() => {
let container = ref.current;
if (!container) return;

let codeBlocks = container.querySelectorAll(
"[data-code-block][data-lang]:not([data-nocopy])"
);
let buttons = new Map<
HTMLButtonElement,
{ listener: (event: MouseEvent) => void; to: number }
>();

for (let codeBlock of codeBlocks) {
let button = document.createElement("button");
let label = document.createElement("span");
button.type = "button";
button.dataset.codeBlockCopy = "";
button.addEventListener("click", listener);

label.textContent = "Copy code to clipboard";
label.classList.add("sr-only");
button.appendChild(label);
codeBlock.appendChild(button);
buttons.set(button, { listener, to: -1 });

function listener(event: MouseEvent) {
event.preventDefault();
let pre = codeBlock.querySelector("pre");
let text = pre?.textContent;
if (!text) return;
navigator.clipboard
.writeText(text)
.then(() => {
button.dataset.copied = "true";
let to = window.setTimeout(() => {
window.clearTimeout(to);
if (button) {
button.dataset.copied = undefined;
}
}, 3000);
if (buttons.has(button)) {
buttons.get(button)!.to = to;
}
})
.catch((error) => {
console.error(error);
});
}
}
return () => {
for (let [button, props] of buttons) {
button.removeEventListener("click", props.listener);
button.parentElement?.removeChild(button);
window.clearTimeout(props.to);
}
};
}, [ref, location.pathname]);
}

0 comments on commit c4a4640

Please sign in to comment.