11import { VERSION } from "../constants.js" ;
2-
3- const REACT_GRAB_MIME_TYPE = "application/x-react-grab" ;
2+ import { renderTextToImage } from "./render-text-to-image.js" ;
43
54export interface ReactGrabEntry {
65 tagName ?: string ;
@@ -31,10 +30,13 @@ const escapeHtml = (text: string): string =>
3130 . replace ( / > / g, ">" )
3231 . replace ( / " / g, """ ) ;
3332
34- export const copyContent = (
33+ const buildHtmlPayload = ( content : string ) : string =>
34+ `<meta charset='utf-8'><pre><code>${ escapeHtml ( content ) } </code></pre>` ;
35+
36+ const buildMetadata = (
3537 content : string ,
3638 options ?: CopyContentOptions ,
37- ) : boolean => {
39+ ) : ReactGrabMetadata => {
3840 const elementName = options ?. componentName ?? "div" ;
3941 const entries = options ?. entries ?? [
4042 {
@@ -44,23 +46,37 @@ export const copyContent = (
4446 commentText : options ?. commentText ,
4547 } ,
4648 ] ;
47- const reactGrabMetadata : ReactGrabMetadata = {
48- version : VERSION ,
49- content,
50- entries,
51- timestamp : Date . now ( ) ,
52- } ;
49+ return { version : VERSION , content, entries, timestamp : Date . now ( ) } ;
50+ } ;
51+
52+ const isModernClipboardAvailable = ( ) : boolean =>
53+ Boolean ( navigator . clipboard ?. write ) && typeof ClipboardItem !== "undefined" ;
5354
55+ /**
56+ * Modern path: writes text/plain + text/html + image/png in a single ClipboardItem.
57+ * Cannot carry custom MIME types like application/x-react-grab.
58+ */
59+ const modernCopy = async ( content : string ) : Promise < void > => {
60+ const item = new ClipboardItem ( {
61+ "text/plain" : new Blob ( [ content ] , { type : "text/plain" } ) ,
62+ "text/html" : new Blob ( [ buildHtmlPayload ( content ) ] , { type : "text/html" } ) ,
63+ "image/png" : renderTextToImage ( content ) ,
64+ } ) ;
65+ await navigator . clipboard . write ( [ item ] ) ;
66+ } ;
67+
68+ /**
69+ * Legacy path: execCommand("copy") with text/plain + text/html + metadata.
70+ * Must run synchronously within a user gesture call stack.
71+ */
72+ const legacyCopy = ( content : string , metadata : ReactGrabMetadata ) : boolean => {
5473 const copyHandler = ( event : ClipboardEvent ) => {
5574 event . preventDefault ( ) ;
5675 event . clipboardData ?. setData ( "text/plain" , content ) ;
76+ event . clipboardData ?. setData ( "text/html" , buildHtmlPayload ( content ) ) ;
5777 event . clipboardData ?. setData (
58- "text/html" ,
59- `<meta charset='utf-8'><pre><code>${ escapeHtml ( content ) } </code></pre>` ,
60- ) ;
61- event . clipboardData ?. setData (
62- REACT_GRAB_MIME_TYPE ,
63- JSON . stringify ( reactGrabMetadata ) ,
78+ "application/x-react-grab" ,
79+ JSON . stringify ( metadata ) ,
6480 ) ;
6581 } ;
6682
@@ -78,13 +94,30 @@ export const copyContent = (
7894 if ( typeof document . execCommand !== "function" ) {
7995 return false ;
8096 }
81- const didCopySucceed = document . execCommand ( "copy" ) ;
82- if ( didCopySucceed ) {
83- options ?. onSuccess ?.( ) ;
84- }
85- return didCopySucceed ;
97+ return document . execCommand ( "copy" ) ;
8698 } finally {
8799 document . removeEventListener ( "copy" , copyHandler ) ;
88100 textarea . remove ( ) ;
89101 }
90102} ;
103+
104+ export const copyContent = async (
105+ content : string ,
106+ options ?: CopyContentOptions ,
107+ ) : Promise < boolean > => {
108+ let didCopy : boolean ;
109+
110+ if ( isModernClipboardAvailable ( ) ) {
111+ try {
112+ await modernCopy ( content ) ;
113+ didCopy = true ;
114+ } catch {
115+ didCopy = false ;
116+ }
117+ } else {
118+ didCopy = legacyCopy ( content , buildMetadata ( content , options ) ) ;
119+ }
120+
121+ if ( didCopy ) options ?. onSuccess ?.( ) ;
122+ return didCopy ;
123+ } ;
0 commit comments