Skip to content

Commit 2baa842

Browse files
author
javatcoding1
committed
fix(copy-code): improve error handling and button feedback
1 parent 7a7d335 commit 2baa842

File tree

2 files changed

+64
-73
lines changed

2 files changed

+64
-73
lines changed

src/utils/MarkdownRenderer.tsx

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import rehypeSlug from 'rehype-slug';
99
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
1010
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
1111
import { h } from 'hastscript';
12+
import { initCodeCopy } from '@/utils/copy-code';
1213

1314
// Type definitions
1415
interface MarkdownRendererProps {
@@ -226,35 +227,7 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
226227
}
227228
}, []);
228229

229-
const handleCopyClick = useCallback(async (e: MouseEvent) => {
230-
const button = (e.target as HTMLElement).closest(
231-
'.copy-code-btn',
232-
) as HTMLButtonElement | null;
233-
if (!button) return;
234-
235-
const code = button.getAttribute('data-code') || '';
236-
237-
try {
238-
await navigator.clipboard.writeText(code);
239-
const successMessage = button.nextElementSibling as HTMLElement;
240-
if (successMessage) {
241-
successMessage.classList.remove('hidden');
242-
successMessage.classList.add('flex');
243-
setTimeout(() => {
244-
successMessage.classList.add('hidden');
245-
successMessage.classList.remove('flex');
246-
}, 2000);
247-
}
248-
} catch {
249-
const textArea = document.createElement('textarea');
250-
textArea.value = code;
251-
textArea.style.cssText = 'position:fixed;opacity:0;';
252-
document.body.appendChild(textArea);
253-
textArea.select();
254-
document.execCommand('copy');
255-
document.body.removeChild(textArea);
256-
}
257-
}, []);
230+
258231

259232
const handleAnchorClick = useCallback((e: MouseEvent) => {
260233
const target = e.target as HTMLElement;
@@ -281,13 +254,11 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
281254
}, []);
282255

283256
useEffect(() => {
284-
document.addEventListener('click', handleCopyClick);
285257
document.addEventListener('click', handleAnchorClick);
286258
return () => {
287-
document.removeEventListener('click', handleCopyClick);
288259
document.removeEventListener('click', handleAnchorClick);
289260
};
290-
}, [handleCopyClick, handleAnchorClick]);
261+
}, [handleAnchorClick]);
291262

292263
useEffect(() => {
293264
if (setZoomableImages) {
@@ -296,6 +267,15 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
296267
}
297268
}, [processedContent, setZoomableImages]);
298269

270+
// Initialize copy functionality when content changes
271+
useEffect(() => {
272+
const timeoutId = setTimeout(() => {
273+
initCodeCopy();
274+
}, 100); // Small delay to ensure DOM is updated
275+
276+
return () => clearTimeout(timeoutId);
277+
}, [processedContent]);
278+
299279
useEffect(() => {
300280
scrollToAnchor();
301281
const handlePopState = () => {
@@ -403,9 +383,9 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
403383
{language}
404384
</span>
405385
</div>
406-
<div className="flex items-center space-x-2">
386+
<div className="flex items-center">
407387
<button
408-
className="copy-code-btn bg-gray-200 hover:bg-gray-300 text-gray-700 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 hover:scale-105 shadow-sm hover:shadow-md"
388+
className="copy-code-btn bg-gray-200 hover:bg-gray-300 text-gray-700 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 hover:scale-105 shadow-sm hover:shadow-md"
409389
data-code={codeText}
410390
aria-label="Copy code to clipboard"
411391
>
@@ -424,9 +404,6 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
424404
</svg>
425405
<span className="font-medium">Copy</span>
426406
</button>
427-
<div className="copy-success-message hidden items-center space-x-2 bg-green-100 text-green-800 text-xs px-4 py-2 rounded-lg border border-green-200">
428-
<span className="font-medium">Copied!</span>
429-
</div>
430407
</div>
431408
</div>
432409
<div className="overflow-x-auto">

src/utils/copy-code.ts

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
2-
* Copy code functionality for code blocks
3-
* Simplified and optimized version
2+
* Copy code functionality for code blocks with user-friendly feedback
43
*/
54

65
/**
76
* Handle click on copy button with unified clipboard handling
7+
* @param event - The click event from the copy button
88
*/
99
function handleCopyClick(event: Event): void {
1010
event.preventDefault();
@@ -13,55 +13,76 @@ function handleCopyClick(event: Event): void {
1313
const button = event.currentTarget as HTMLElement;
1414
const codeContent = button.getAttribute('data-code');
1515

16-
if (!codeContent) {
17-
console.error('No code content found to copy');
16+
if (!codeContent || codeContent.trim() === '') {
17+
showErrorMessage(button);
1818
return;
1919
}
2020

2121
copyToClipboard(codeContent, button);
2222
}
2323

24-
/**
25-
* Unified clipboard copy with fallback
26-
*/
27-
async function copyToClipboard(
28-
text: string,
29-
button: HTMLElement,
30-
): Promise<void> {
24+
async function copyToClipboard(text: string, button: HTMLElement): Promise<void> {
3125
try {
3226
await navigator.clipboard.writeText(text);
3327
showSuccessMessage(button);
3428
} catch {
3529
// Fallback for older browsers
3630
const textarea = document.createElement('textarea');
3731
textarea.value = text;
38-
textarea.style.cssText =
39-
'position:fixed;left:-999999px;top:-999999px;opacity:0;';
40-
32+
textarea.style.cssText = 'position:fixed;left:-999999px;top:-999999px;opacity:0;';
4133
document.body.appendChild(textarea);
4234
textarea.select();
43-
4435
const success = document.execCommand('copy');
4536
document.body.removeChild(textarea);
46-
47-
if (success) showSuccessMessage(button);
37+
38+
if (success) {
39+
showSuccessMessage(button);
40+
} else {
41+
showErrorMessage(button);
42+
}
4843
}
4944
}
5045

51-
/**
52-
* Show success message
53-
*/
5446
function showSuccessMessage(button: HTMLElement): void {
55-
const successMessage = button.nextElementSibling as HTMLElement;
56-
if (successMessage?.classList.contains('copy-success-message')) {
57-
successMessage.classList.remove('hidden');
58-
setTimeout(() => successMessage.classList.add('hidden'), 2000);
59-
}
47+
const originalContent = button.innerHTML;
48+
const originalClasses = button.className;
49+
50+
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';
51+
button.innerHTML = `
52+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
54+
</svg>
55+
<span class="font-medium">Copied!</span>
56+
`;
57+
button.setAttribute('disabled', 'true');
58+
59+
setTimeout(() => {
60+
button.className = originalClasses;
61+
button.innerHTML = originalContent;
62+
button.removeAttribute('disabled');
63+
}, 2000);
64+
}
65+
66+
function showErrorMessage(button: HTMLElement): void {
67+
const originalContent = button.innerHTML;
68+
const originalClasses = button.className;
69+
70+
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';
71+
button.innerHTML = `
72+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
73+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
74+
</svg>
75+
<span class="font-medium">Failed!</span>
76+
`;
77+
button.setAttribute('disabled', 'true');
78+
79+
setTimeout(() => {
80+
button.className = originalClasses;
81+
button.innerHTML = originalContent;
82+
button.removeAttribute('disabled');
83+
}, 2500);
6084
}
6185

62-
/**
63-
* Initialize copy code functionality
64-
*/
6586
export function initCodeCopy(): void {
6687
document.querySelectorAll('.copy-code-btn').forEach((button) => {
6788
if (button instanceof HTMLElement) {
@@ -71,18 +92,11 @@ export function initCodeCopy(): void {
7192
});
7293
}
7394

74-
// Auto-initialize when available
95+
// Auto-initialize
7596
if (typeof window !== 'undefined') {
7697
if (document.readyState === 'loading') {
7798
document.addEventListener('DOMContentLoaded', initCodeCopy);
7899
} else {
79100
initCodeCopy();
80101
}
81-
82-
// Re-initialize for dynamic content
83-
let timeoutId: number;
84-
document.addEventListener('click', () => {
85-
clearTimeout(timeoutId);
86-
timeoutId = window.setTimeout(initCodeCopy, 100);
87-
});
88102
}

0 commit comments

Comments
 (0)