@@ -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/
124111async 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