diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 83b0f2b86b..39d8679792 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -96,10 +96,7 @@ const { ) //copy README file as Markdown -const { copied: copiedReadme, copy: copyReadme } = useClipboard({ - source: () => '', - copiedDuring: 2000, -}) +const copiedReadme = shallowRef(false) function prefetchReadmeMarkdown() { if (readmeMarkdownStatus.value === 'idle') { @@ -107,13 +104,52 @@ function prefetchReadmeMarkdown() { } } +// Safari requires clipboard writes synchronously within a user gesture. +// Passing a Promise into ClipboardItem lets clipboard.write() stay +// synchronous while the fetch resolves asynchronously inside the item. +// See: https://wolfgangrittner.dev/how-to-use-clipboard-api-in-safari/ async function copyReadmeHandler() { - await fetchReadmeMarkdown() - - const markdown = readmeMarkdownData.value?.markdown - if (!markdown) return + let copied = false + + try { + const item = new ClipboardItem({ + 'text/plain': (async () => { + await fetchReadmeMarkdown() + const markdown = readmeMarkdownData.value?.markdown + if (!markdown) throw new Error('No markdown') + return new Blob([markdown], { type: 'text/plain' }) + })(), + }) + await navigator.clipboard.write([item]) + copied = true + } catch { + // Fallback for browsers without ClipboardItem Promise support + await fetchReadmeMarkdown() + const markdown = readmeMarkdownData.value?.markdown + if (markdown) { + try { + await navigator.clipboard.writeText(markdown) + copied = true + } catch { + // last resort: execCommand + const textarea = document.createElement('textarea') + textarea.value = markdown + textarea.style.position = 'fixed' + textarea.style.opacity = '0' + document.body.appendChild(textarea) + textarea.select() + copied = document.execCommand('copy') + document.body.removeChild(textarea) + } + } + } - await copyReadme(markdown) + if (copied) { + copiedReadme.value = true + setTimeout(() => { + copiedReadme.value = false + }, 2000) + } } // Track active TOC item based on scroll position