@@ -17,6 +17,18 @@ import { useTranslation } from '@/languages/components/useTranslation'
1717import cx from 'clsx'
1818import styles from './ViewMarkdownButton.module.scss'
1919
20+ async function fetchMarkdown ( markdownUrl : string ) {
21+ const res = await fetch ( markdownUrl )
22+ if ( ! res . ok ) {
23+ throw new Error ( `Failed to fetch: ${ res . status } ` )
24+ }
25+ return res . text ( )
26+ }
27+
28+ async function fetchMarkdownBlob ( markdownUrl : string ) {
29+ return new Blob ( [ await fetchMarkdown ( markdownUrl ) ] , { type : 'text/plain' } )
30+ }
31+
2032interface CopyMarkdownMenuProps {
2133 currentPath : string
2234}
@@ -30,6 +42,7 @@ export const CopyMarkdownMenu = ({ currentPath }: CopyMarkdownMenuProps) => {
3042
3143 const [ copied , setCopied ] = useState ( false )
3244 const timeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
45+ const copyAttemptRef = useRef ( 0 )
3346
3447 useEffect ( ( ) => {
3548 return ( ) => {
@@ -71,18 +84,22 @@ export const CopyMarkdownMenu = ({ currentPath }: CopyMarkdownMenuProps) => {
7184 clipboard_target : markdownUrl ,
7285 eventGroupKey : MARKDOWN_SOURCE_MENU_EVENT_GROUP ,
7386 } )
87+ const attempt = ++ copyAttemptRef . current
7488 try {
75- const res = await fetch ( markdownUrl )
76- if ( ! res . ok ) {
77- throw new Error ( `Failed to fetch: ${ res . status } ` )
89+ if ( typeof ClipboardItem !== 'undefined' && typeof navigator . clipboard . write === 'function' ) {
90+ // Safari rejects clipboard writes that start after an await in a click handler, so start the write before the fetch resolves.
91+ const blob = fetchMarkdownBlob ( markdownUrl )
92+ await navigator . clipboard . write ( [ new ClipboardItem ( { 'text/plain' : blob } ) ] )
93+ } else {
94+ await navigator . clipboard . writeText ( await fetchMarkdown ( markdownUrl ) )
7895 }
79- const text = await res . text ( )
80- await navigator . clipboard . writeText ( text )
8196 announce ( t ( 'copied' ) )
8297 setCopied ( true )
8398 if ( timeoutRef . current ) clearTimeout ( timeoutRef . current )
8499 timeoutRef . current = setTimeout ( ( ) => setCopied ( false ) , 2000 )
85100 } catch {
101+ // Safari rejects a pending clipboard write when a newer click starts another, so a superseded attempt skips the fallback.
102+ if ( attempt !== copyAttemptRef . current ) return
86103 // If fetch or clipboard fails, open the markdown source in a new tab.
87104 window . open ( markdownUrl , '_blank' )
88105 }
0 commit comments