@@ -216,6 +216,21 @@ export class SelectionManager {
216216 ) ;
217217 }
218218
219+ /**
220+ * Copy the current selection to clipboard
221+ * @returns true if there was text to copy, false otherwise
222+ */
223+ copySelection ( ) : boolean {
224+ if ( ! this . hasSelection ( ) ) return false ;
225+
226+ const text = this . getSelection ( ) ;
227+ if ( text ) {
228+ this . copyToClipboard ( text ) ;
229+ return true ;
230+ }
231+ return false ;
232+ }
233+
219234 /**
220235 * Clear the selection
221236 */
@@ -841,26 +856,72 @@ export class SelectionManager {
841856
842857 /**
843858 * Copy text to clipboard
859+ *
860+ * Strategy (modern APIs first):
861+ * 1. Try ClipboardItem API (works in Safari and modern browsers)
862+ * - Safari requires the ClipboardItem to be created synchronously within user gesture
863+ * 2. Try navigator.clipboard.writeText (modern async API, may fail in Safari)
864+ * 3. Fall back to execCommand (legacy, for older browsers)
844865 */
845- private async copyToClipboard ( text : string ) : Promise < void > {
846- // First try: modern async clipboard API
847- if ( navigator . clipboard && navigator . clipboard . writeText ) {
866+ private copyToClipboard ( text : string ) : void {
867+ // First try: ClipboardItem API (modern, Safari-compatible)
868+ // Safari allows this because we create the ClipboardItem synchronously
869+ // within the user gesture, even though the write is async
870+ if ( navigator . clipboard && typeof ClipboardItem !== 'undefined' ) {
848871 try {
849- await navigator . clipboard . writeText ( text ) ;
872+ const blob = new Blob ( [ text ] , { type : 'text/plain' } ) ;
873+ const clipboardItem = new ClipboardItem ( {
874+ 'text/plain' : blob ,
875+ } ) ;
876+ navigator . clipboard . write ( [ clipboardItem ] ) . catch ( ( err ) => {
877+ console . warn ( 'ClipboardItem write failed, trying writeText:' , err ) ;
878+ // Try writeText as fallback
879+ this . copyWithWriteText ( text ) ;
880+ } ) ;
850881 return ;
851882 } catch ( err ) {
852- // Clipboard API failed (common in non-HTTPS or non-focused contexts)
853- // Fall through to legacy method
883+ // ClipboardItem not supported or failed, fall through
854884 }
855885 }
856886
857- // Second try: legacy execCommand method via textarea
887+ // Second try: basic async writeText (works in Chrome, may fail in Safari)
888+ if ( navigator . clipboard && navigator . clipboard . writeText ) {
889+ navigator . clipboard . writeText ( text ) . catch ( ( err ) => {
890+ console . warn ( 'Clipboard writeText failed, trying execCommand:' , err ) ;
891+ // Fall back to execCommand
892+ this . copyWithExecCommand ( text ) ;
893+ } ) ;
894+ return ;
895+ }
896+
897+ // Third try: legacy execCommand fallback
898+ this . copyWithExecCommand ( text ) ;
899+ }
900+
901+ /**
902+ * Copy using navigator.clipboard.writeText
903+ */
904+ private copyWithWriteText ( text : string ) : void {
905+ if ( navigator . clipboard && navigator . clipboard . writeText ) {
906+ navigator . clipboard . writeText ( text ) . catch ( ( err ) => {
907+ console . warn ( 'Clipboard writeText failed, trying execCommand:' , err ) ;
908+ this . copyWithExecCommand ( text ) ;
909+ } ) ;
910+ } else {
911+ this . copyWithExecCommand ( text ) ;
912+ }
913+ }
914+
915+ /**
916+ * Copy using legacy execCommand (fallback for older browsers)
917+ */
918+ private copyWithExecCommand ( text : string ) : void {
858919 const previouslyFocused = document . activeElement as HTMLElement ;
859920 try {
860921 // Position textarea offscreen but in a way that allows selection
861922 const textarea = this . textarea ;
862923 textarea . value = text ;
863- textarea . style . position = 'fixed' ; // Avoid scrolling to bottom
924+ textarea . style . position = 'fixed' ;
864925 textarea . style . left = '-9999px' ;
865926 textarea . style . top = '0' ;
866927 textarea . style . width = '1px' ;
@@ -880,11 +941,11 @@ export class SelectionManager {
880941 }
881942
882943 if ( ! success ) {
883- console . error ( '❌ execCommand copy failed') ;
944+ console . warn ( ' execCommand copy failed') ;
884945 }
885946 } catch ( err ) {
886- console . error ( '❌ Fallback copy failed :', err ) ;
887- // Still try to restore focus even on error
947+ console . warn ( 'execCommand copy threw :', err ) ;
948+ // Restore focus on error
888949 if ( previouslyFocused ) {
889950 previouslyFocused . focus ( ) ;
890951 }
0 commit comments