* Copy text to clipboard
(text: string)
| 843 | * Copy text to clipboard |
| 844 | */ |
| 845 | private async copyToClipboard(text: string): Promise<void> { |
| 846 | // First try: modern async clipboard API |
| 847 | if (navigator.clipboard && navigator.clipboard.writeText) { |
| 848 | try { |
| 849 | await navigator.clipboard.writeText(text); |
| 850 | return; |
| 851 | } catch (err) { |
| 852 | // Clipboard API failed (common in non-HTTPS or non-focused contexts) |
| 853 | // Fall through to legacy method |
| 854 | } |
| 855 | } |
| 856 | |
| 857 | // Second try: legacy execCommand method via textarea |
| 858 | const previouslyFocused = document.activeElement as HTMLElement; |
| 859 | try { |
| 860 | // Position textarea offscreen but in a way that allows selection |
| 861 | const textarea = this.textarea; |
| 862 | textarea.value = text; |
| 863 | textarea.style.position = 'fixed'; // Avoid scrolling to bottom |
| 864 | textarea.style.left = '-9999px'; |
| 865 | textarea.style.top = '0'; |
| 866 | textarea.style.width = '1px'; |
| 867 | textarea.style.height = '1px'; |
| 868 | textarea.style.opacity = '0'; |
| 869 | |
| 870 | // Select all text and copy |
| 871 | textarea.focus(); |
| 872 | textarea.select(); |
| 873 | textarea.setSelectionRange(0, text.length); |
| 874 | |
| 875 | const success = document.execCommand('copy'); |
| 876 | |
| 877 | // Restore focus |
| 878 | if (previouslyFocused) { |
| 879 | previouslyFocused.focus(); |
| 880 | } |
| 881 | |
| 882 | if (!success) { |
| 883 | console.error('❌ execCommand copy failed'); |
| 884 | } |
| 885 | } catch (err) { |
| 886 | console.error('❌ Fallback copy failed:', err); |
| 887 | // Still try to restore focus even on error |
| 888 | if (previouslyFocused) { |
| 889 | previouslyFocused.focus(); |
| 890 | } |
| 891 | } |
| 892 | } |
| 893 | |
| 894 | /** |
| 895 | * Request a render update (triggers selection overlay redraw) |
no test coverage detected