diff --git a/src/utils/MarkdownRenderer.tsx b/src/utils/MarkdownRenderer.tsx index 7c267019..b62e29cd 100644 --- a/src/utils/MarkdownRenderer.tsx +++ b/src/utils/MarkdownRenderer.tsx @@ -9,6 +9,7 @@ import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import { h } from 'hastscript'; +import { initCodeCopy } from '@/utils/copy-code'; // Type definitions interface MarkdownRendererProps { @@ -226,36 +227,6 @@ const MarkdownRenderer: React.FC = ({ } }, []); - const handleCopyClick = useCallback(async (e: MouseEvent) => { - const button = (e.target as HTMLElement).closest( - '.copy-code-btn', - ) as HTMLButtonElement | null; - if (!button) return; - - const code = button.getAttribute('data-code') || ''; - - try { - await navigator.clipboard.writeText(code); - const successMessage = button.nextElementSibling as HTMLElement; - if (successMessage) { - successMessage.classList.remove('hidden'); - successMessage.classList.add('flex'); - setTimeout(() => { - successMessage.classList.add('hidden'); - successMessage.classList.remove('flex'); - }, 2000); - } - } catch { - const textArea = document.createElement('textarea'); - textArea.value = code; - textArea.style.cssText = 'position:fixed;opacity:0;'; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - } - }, []); - const handleAnchorClick = useCallback((e: MouseEvent) => { const target = e.target as HTMLElement; @@ -281,13 +252,11 @@ const MarkdownRenderer: React.FC = ({ }, []); useEffect(() => { - document.addEventListener('click', handleCopyClick); document.addEventListener('click', handleAnchorClick); return () => { - document.removeEventListener('click', handleCopyClick); document.removeEventListener('click', handleAnchorClick); }; - }, [handleCopyClick, handleAnchorClick]); + }, [handleAnchorClick]); useEffect(() => { if (setZoomableImages) { @@ -296,6 +265,15 @@ const MarkdownRenderer: React.FC = ({ } }, [processedContent, setZoomableImages]); + // Initialize copy functionality when content changes + useEffect(() => { + const timeoutId = setTimeout(() => { + initCodeCopy(); + }, 100); // Small delay to ensure DOM is updated + + return () => clearTimeout(timeoutId); + }, [processedContent]); + useEffect(() => { scrollToAnchor(); const handlePopState = () => { @@ -403,9 +381,9 @@ const MarkdownRenderer: React.FC = ({ {language} -
+
-
- Copied! -
diff --git a/src/utils/copy-code.ts b/src/utils/copy-code.ts index a2733e87..d97b8f18 100644 --- a/src/utils/copy-code.ts +++ b/src/utils/copy-code.ts @@ -1,10 +1,10 @@ /** - * Copy code functionality for code blocks - * Simplified and optimized version + * Copy code functionality for code blocks with user-friendly feedback */ /** * Handle click on copy button with unified clipboard handling + * @param event - The click event from the copy button */ function handleCopyClick(event: Event): void { event.preventDefault(); @@ -13,8 +13,8 @@ function handleCopyClick(event: Event): void { const button = event.currentTarget as HTMLElement; const codeContent = button.getAttribute('data-code'); - if (!codeContent) { - console.error('No code content found to copy'); + if (!codeContent || codeContent.trim() === '') { + showErrorMessage(button); return; } @@ -22,7 +22,9 @@ function handleCopyClick(event: Event): void { } /** - * Unified clipboard copy with fallback + * Copy text to clipboard with fallback for older browsers + * @param text - The text content to copy to clipboard + * @param button - The copy button element for showing feedback */ async function copyToClipboard( text: string, @@ -37,30 +39,72 @@ async function copyToClipboard( textarea.value = text; textarea.style.cssText = 'position:fixed;left:-999999px;top:-999999px;opacity:0;'; - document.body.appendChild(textarea); textarea.select(); - const success = document.execCommand('copy'); document.body.removeChild(textarea); - if (success) showSuccessMessage(button); + if (success) { + showSuccessMessage(button); + } else { + showErrorMessage(button); + } } } /** - * Show success message + * Show success feedback by transforming button to green "Copied!" state + * @param button - The copy button element to show success feedback in */ function showSuccessMessage(button: HTMLElement): void { - const successMessage = button.nextElementSibling as HTMLElement; - if (successMessage?.classList.contains('copy-success-message')) { - successMessage.classList.remove('hidden'); - setTimeout(() => successMessage.classList.add('hidden'), 2000); - } + const originalContent = button.innerHTML; + const originalClasses = button.className; + + button.className = + 'copy-code-btn bg-green-100 text-green-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-green-200'; + button.innerHTML = ` + + + + Copied! + `; + button.setAttribute('disabled', 'true'); + + setTimeout(() => { + button.className = originalClasses; + button.innerHTML = originalContent; + button.removeAttribute('disabled'); + }, 2000); +} + +/** + * Show error feedback by transforming button to red "Failed!" state + * @param button - The copy button element to show error feedback in + */ +function showErrorMessage(button: HTMLElement): void { + const originalContent = button.innerHTML; + const originalClasses = button.className; + + button.className = + 'copy-code-btn bg-red-100 text-red-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-red-200'; + button.innerHTML = ` + + + + Failed! + `; + button.setAttribute('disabled', 'true'); + + setTimeout(() => { + button.className = originalClasses; + button.innerHTML = originalContent; + button.removeAttribute('disabled'); + }, 2500); } /** - * Initialize copy code functionality + * Initialize copy functionality for all copy buttons on the page + * Attaches click event handlers to elements with 'copy-code-btn' class */ export function initCodeCopy(): void { document.querySelectorAll('.copy-code-btn').forEach((button) => { @@ -71,18 +115,11 @@ export function initCodeCopy(): void { }); } -// Auto-initialize when available +// Auto-initialize if (typeof window !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCodeCopy); } else { initCodeCopy(); } - - // Re-initialize for dynamic content - let timeoutId: number; - document.addEventListener('click', () => { - clearTimeout(timeoutId); - timeoutId = window.setTimeout(initCodeCopy, 100); - }); }