Skip to content

Commit e1790c8

Browse files
howwohmmclaude
andcommitted
fix: Safari clipboard fallback for "Copy as Markdown" README button
Fixes #2151 Safari requires navigator.clipboard.writeText() to be called synchronously within a user gesture. The async fetchReadmeMarkdown() breaks that chain, causing the clipboard write to silently fail. Added a document.execCommand('copy') fallback via a temporary textarea when the Clipboard API rejects. Also bypassed useClipboard's copy() to directly use navigator.clipboard.writeText() so the rejection is catchable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f2fc1a commit e1790c8

File tree

1 file changed

+34
-1
lines changed

1 file changed

+34
-1
lines changed

app/pages/package/[[org]]/[name].vue

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,46 @@ function prefetchReadmeMarkdown() {
107107
}
108108
}
109109
110+
// Fallback for Safari: navigator.clipboard.writeText() must be called
111+
// synchronously within a user gesture. The async fetch breaks that chain,
112+
// so we fall back to execCommand('copy') via a temporary textarea.
113+
function copyViaExecCommand(text: string): boolean {
114+
const textarea = document.createElement('textarea')
115+
textarea.value = text
116+
textarea.style.position = 'fixed'
117+
textarea.style.opacity = '0'
118+
document.body.appendChild(textarea)
119+
textarea.select()
120+
try {
121+
return document.execCommand('copy')
122+
}
123+
finally {
124+
document.body.removeChild(textarea)
125+
}
126+
}
127+
110128
async function copyReadmeHandler() {
111129
await fetchReadmeMarkdown()
112130
113131
const markdown = readmeMarkdownData.value?.markdown
114132
if (!markdown) return
115133
116-
await copyReadme(markdown)
134+
// Try the modern clipboard API first, then fall back to execCommand.
135+
// Safari requires clipboard writes synchronously within a user gesture —
136+
// the async fetch above breaks that chain, so writeText() will reject.
137+
let success = false
138+
try {
139+
await navigator.clipboard.writeText(markdown)
140+
success = true
141+
}
142+
catch {
143+
success = copyViaExecCommand(markdown)
144+
}
145+
146+
if (success) {
147+
copiedReadme.value = true
148+
setTimeout(() => { copiedReadme.value = false }, 2000)
149+
}
117150
}
118151
119152
// Track active TOC item based on scroll position

0 commit comments

Comments
 (0)