Skip to content

Commit d0818ef

Browse files
committed
fix: copy may fail on certain devices
1 parent ff52f97 commit d0818ef

1 file changed

Lines changed: 79 additions & 11 deletions

File tree

components/CopyRawMdxButton.tsx

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,80 @@ export default function CopyRawMdxButton() {
88
const [isCopying, setIsCopying] = React.useState(false);
99
const [isCopied, setIsCopied] = React.useState(false);
1010
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
11+
const [prefetched, setPrefetched] = React.useState<string | null>(null);
12+
const [isPrefetching, setIsPrefetching] = React.useState(false);
1113

12-
const handleCopy = async () => {
14+
// Prefetch the MDX content early so copy can run synchronously on click (iOS Safari requirement)
15+
const prefetch = React.useCallback(async () => {
16+
if (prefetched != null || isPrefetching) return;
1317
try {
14-
setIsCopying(true);
15-
setErrorMessage(null);
16-
const response = await fetch("/api/raw-mdx?doc=js_api");
17-
if (!response.ok) {
18-
const maybeJson = await response
18+
setIsPrefetching(true);
19+
const res = await fetch("/api/raw-mdx?doc=js_api");
20+
if (!res.ok) {
21+
const maybeJson = await res
1922
.json()
20-
.catch(() => ({ message: `HTTP ${response.status}` }));
21-
throw new Error(maybeJson.message || `HTTP ${response.status}`);
23+
.catch(() => ({ message: `HTTP ${res.status}` }));
24+
throw new Error(maybeJson.message || `HTTP ${res.status}`);
25+
}
26+
const text = await res.text();
27+
setPrefetched(text);
28+
} catch (e) {
29+
// Don't surface prefetch errors loudly; user may still retry
30+
// We'll show a message if copy is attempted without available content
31+
console.error("Prefetch raw MDX failed:", e);
32+
} finally {
33+
setIsPrefetching(false);
34+
}
35+
}, [prefetched, isPrefetching]);
36+
37+
React.useEffect(() => {
38+
// Start prefetch when the button mounts
39+
void prefetch();
40+
}, [prefetch]);
41+
42+
const copyWithFallback = React.useCallback(async (text: string) => {
43+
// Try modern Clipboard API first
44+
try {
45+
if (navigator.clipboard && "writeText" in navigator.clipboard) {
46+
await navigator.clipboard.writeText(text);
47+
return true;
2248
}
23-
const content = await response.text();
24-
await navigator.clipboard.writeText(content);
49+
} catch (e) {
50+
// Continue to fallback below
51+
}
52+
// Fallback: use a hidden textarea + execCommand within the click handler call stack
53+
try {
54+
const ta = document.createElement("textarea");
55+
ta.value = text;
56+
ta.setAttribute("readonly", "");
57+
ta.style.position = "fixed"; // avoid scroll jump on iOS
58+
ta.style.top = "-9999px";
59+
ta.style.opacity = "0";
60+
document.body.appendChild(ta);
61+
ta.focus();
62+
ta.select();
63+
const ok = document.execCommand("copy");
64+
document.body.removeChild(ta);
65+
if (!ok) throw new Error("execCommand copy failed");
66+
return true;
67+
} catch (e) {
68+
return false;
69+
}
70+
}, []);
71+
72+
const handleCopy = async () => {
73+
setErrorMessage(null);
74+
// Ensure we have content ready before attempting to copy to meet iOS Safari's user-gesture requirement
75+
if (prefetched == null) {
76+
// Kick off prefetch (if not already) and ask user to tap again
77+
void prefetch();
78+
setErrorMessage("Preparing content… tap again to copy");
79+
return;
80+
}
81+
setIsCopying(true);
82+
try {
83+
const ok = await copyWithFallback(prefetched);
84+
if (!ok) throw new Error("Copy not allowed in this context");
2585
setIsCopied(true);
2686
setTimeout(() => setIsCopied(false), 2000);
2787
} catch (error) {
@@ -33,7 +93,15 @@ export default function CopyRawMdxButton() {
3393

3494
return (
3595
<div className="flex items-center gap-2 mt-6">
36-
<Button onClick={handleCopy} disabled={isCopying} variant="secondary" size="sm" title="Copy raw Markdown (about 35K tokens)">
96+
<Button
97+
onClick={handleCopy}
98+
onMouseEnter={prefetch}
99+
onTouchStart={prefetch}
100+
disabled={isCopying}
101+
variant="secondary"
102+
size="sm"
103+
title="Copy raw Markdown (about 35K tokens)"
104+
>
37105
{isCopying ? (
38106
<>
39107
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading...

0 commit comments

Comments
 (0)