Skip to content

Commit e3117a0

Browse files
howwohmmclaude
andcommitted
fix: use ClipboardItem with Promise for Safari clipboard support
Replace writeText() + execCommand fallback with the ClipboardItem Promise pattern. Passing the async fetch as a Promise into ClipboardItem keeps the clipboard.write() call synchronous within the user gesture, which is what Safari requires. Ref: https://wolfgangrittner.dev/how-to-use-clipboard-api-in-safari/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent df4dfe9 commit e3117a0

File tree

1 file changed

+38
-36
lines changed

1 file changed

+38
-36
lines changed

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

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -104,45 +104,47 @@ function prefetchReadmeMarkdown() {
104104
}
105105
}
106106
107-
// Fallback for Safari: navigator.clipboard.writeText() must be called
108-
// synchronously within a user gesture. The async fetch breaks that chain,
109-
// so we fall back to execCommand('copy') via a temporary textarea.
110-
function copyViaExecCommand(text: string): boolean {
111-
const textarea = document.createElement('textarea')
112-
textarea.value = text
113-
textarea.style.position = 'fixed'
114-
textarea.style.opacity = '0'
115-
document.body.appendChild(textarea)
116-
textarea.select()
117-
try {
118-
return document.execCommand('copy')
119-
} finally {
120-
document.body.removeChild(textarea)
121-
}
122-
}
123-
107+
// Safari requires clipboard writes synchronously within a user gesture.
108+
// Passing a Promise into ClipboardItem lets clipboard.write() stay
109+
// synchronous while the fetch resolves asynchronously inside the item.
110+
// See: https://wolfgangrittner.dev/how-to-use-clipboard-api-in-safari/
124111
async function copyReadmeHandler() {
125-
await fetchReadmeMarkdown()
126-
127-
const markdown = readmeMarkdownData.value?.markdown
128-
if (!markdown) return
129-
130-
// Try the modern clipboard API first, then fall back to execCommand.
131-
// Safari requires clipboard writes synchronously within a user gesture —
132-
// the async fetch above breaks that chain, so writeText() will reject.
133-
let success = false
134112
try {
135-
await navigator.clipboard.writeText(markdown)
136-
success = true
137-
} catch {
138-
success = copyViaExecCommand(markdown)
139-
}
140-
141-
if (success) {
113+
const item = new ClipboardItem({
114+
'text/plain': (async () => {
115+
await fetchReadmeMarkdown()
116+
const markdown = readmeMarkdownData.value?.markdown
117+
if (!markdown) throw new Error('No markdown')
118+
return new Blob([markdown], { type: 'text/plain' })
119+
})(),
120+
})
121+
await navigator.clipboard.write([item])
142122
copiedReadme.value = true
143-
setTimeout(() => {
144-
copiedReadme.value = false
145-
}, 2000)
123+
setTimeout(() => { copiedReadme.value = false }, 2000)
124+
} catch {
125+
// Fallback for browsers without ClipboardItem Promise support
126+
await fetchReadmeMarkdown()
127+
const markdown = readmeMarkdownData.value?.markdown
128+
if (!markdown) return
129+
try {
130+
await navigator.clipboard.writeText(markdown)
131+
copiedReadme.value = true
132+
setTimeout(() => { copiedReadme.value = false }, 2000)
133+
} catch {
134+
// last resort: execCommand
135+
const textarea = document.createElement('textarea')
136+
textarea.value = markdown
137+
textarea.style.position = 'fixed'
138+
textarea.style.opacity = '0'
139+
document.body.appendChild(textarea)
140+
textarea.select()
141+
const ok = document.execCommand('copy')
142+
document.body.removeChild(textarea)
143+
if (ok) {
144+
copiedReadme.value = true
145+
setTimeout(() => { copiedReadme.value = false }, 2000)
146+
}
147+
}
146148
}
147149
}
148150

0 commit comments

Comments
 (0)