-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathCopyRawMdxButton.tsx
More file actions
138 lines (129 loc) · 4.95 KB
/
CopyRawMdxButton.tsx
File metadata and controls
138 lines (129 loc) · 4.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Check, Copy, Loader2 } from "lucide-react";
export default function CopyRawMdxButton() {
const [isCopying, setIsCopying] = React.useState(false);
const [isCopied, setIsCopied] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [prefetched, setPrefetched] = React.useState<string | null>(null);
const [isPrefetching, setIsPrefetching] = React.useState(false);
const sources = React.useMemo(
() => ["/raw/js_api.mdx.txt", "/api/raw-mdx?doc=js_api"],
[]
);
// Prefetch the MDX content early so copy can run synchronously on click (iOS Safari requirement)
const prefetch = React.useCallback(async () => {
if (prefetched != null || isPrefetching) return;
try {
setIsPrefetching(true);
let lastError: Error | null = null;
for (const source of sources) {
const res = await fetch(source);
if (!res.ok) {
const maybeJson = await res
.json()
.catch(() => ({ message: `HTTP ${res.status}` }));
lastError = new Error(maybeJson.message || `HTTP ${res.status}`);
continue;
}
const text = await res.text();
setPrefetched(text);
return;
}
throw lastError ?? new Error("No content source responded");
} catch (e) {
// Don't surface prefetch errors loudly; user may still retry
// We'll show a message if copy is attempted without available content
console.error("Prefetch raw MDX failed:", e);
} finally {
setIsPrefetching(false);
}
}, [isPrefetching, prefetched, sources]);
React.useEffect(() => {
// Start prefetch when the button mounts
void prefetch();
}, [prefetch]);
const copyWithFallback = React.useCallback(async (text: string) => {
// Try modern Clipboard API first
try {
if (navigator.clipboard && "writeText" in navigator.clipboard) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) {
// Continue to fallback below
}
// Fallback: use a hidden textarea + execCommand within the click handler call stack
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed"; // avoid scroll jump on iOS
ta.style.top = "-9999px";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.focus();
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
if (!ok) throw new Error("execCommand copy failed");
return true;
} catch (e) {
return false;
}
}, []);
const handleCopy = async () => {
setErrorMessage(null);
// Ensure we have content ready before attempting to copy to meet iOS Safari's user-gesture requirement
if (prefetched == null) {
// Kick off prefetch (if not already) and ask user to tap again
void prefetch();
setErrorMessage("Preparing content… tap again to copy");
return;
}
setIsCopying(true);
try {
const ok = await copyWithFallback(prefetched);
if (!ok) throw new Error("Copy not allowed in this context");
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (error) {
setErrorMessage((error as Error).message);
} finally {
setIsCopying(false);
}
};
return (
<div className="flex items-center gap-2 mt-6">
<Button
onClick={handleCopy}
onMouseEnter={prefetch}
onTouchStart={prefetch}
disabled={isCopying}
variant="secondary"
size="sm"
title="Copy raw Markdown (about 35K tokens)"
>
{isCopying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading...
</>
) : isCopied ? (
<>
<Check className="mr-2 h-4 w-4" /> Copied
</>
) : (
<>
<Copy className="mr-2 h-4 w-4" /> Copy Markdown
</>
)}
</Button>
{errorMessage && (
<span className="text-xs text-red-600" aria-live="polite">
{errorMessage}
</span>
)}
</div>
);
}