|
1 | | -import React, { useEffect } from 'react'; |
| 1 | +import React, { useEffect, useRef, forwardRef } from 'react'; |
2 | 2 | import classNames from 'classnames'; |
3 | 3 | import copy from 'copy-to-clipboard'; |
4 | 4 | import mergeRefs from './utils/mergeRefs'; |
5 | | -import { iconPath as copyPath, svgTpl } from './icons/Copy'; |
6 | | -import { iconPath as checkPath } from './icons/Check'; |
| 5 | +import { iconPath as copyIconPath, svgTpl } from './icons/Copy'; |
| 6 | +import { iconPath as checkIconPath } from './icons/Check'; |
7 | 7 |
|
8 | 8 | interface MarkdownRendererProps extends React.HTMLAttributes<HTMLDivElement> { |
| 9 | + /** |
| 10 | + * Markdown content as HTML string |
| 11 | + */ |
9 | 12 | children?: string | null; |
| 13 | + /** |
| 14 | + * Props to be passed to the copy button |
| 15 | + */ |
10 | 16 | copyButtonProps?: React.HTMLAttributes<HTMLButtonElement>; |
11 | 17 | } |
12 | 18 |
|
13 | | -function appendCopyButton( |
| 19 | +/** |
| 20 | + * Creates and appends a copy button to a code container |
| 21 | + * @param container - The container element to append the copy button to |
| 22 | + * @param buttonProps - Additional props to apply to the copy button |
| 23 | + */ |
| 24 | +function createCopyButton( |
14 | 25 | container?: HTMLDivElement | null, |
15 | 26 | buttonProps?: React.HTMLAttributes<HTMLButtonElement> |
16 | | -) { |
17 | | - if (!container) { |
| 27 | +): void { |
| 28 | + // If the container is null or the container already has a copy button, return |
| 29 | + if (!container || container.querySelector('button[data-type="copy"]')) { |
18 | 30 | return; |
19 | 31 | } |
20 | 32 |
|
| 33 | + const { className, ...rest } = buttonProps || {}; |
21 | 34 | const button = document.createElement('button'); |
22 | | - button.className = 'btn-copy-code'; |
| 35 | + button.dataset['type'] = 'copy'; |
23 | 36 | button.title = 'Copy code'; |
24 | | - button.innerHTML = svgTpl(copyPath); |
| 37 | + button.setAttribute('aria-label', 'Copy code'); |
| 38 | + button.innerHTML = svgTpl(copyIconPath); |
25 | 39 |
|
26 | | - button.onclick = e => { |
| 40 | + if (className) { |
| 41 | + button.className = className; |
| 42 | + } |
| 43 | + |
| 44 | + button.onclick = (e: MouseEvent) => { |
27 | 45 | e.preventDefault(); |
28 | | - const code = container?.querySelector('code')?.textContent; |
| 46 | + const code = container.querySelector('code')?.textContent; |
29 | 47 | const icon = button.querySelector('.copy-icon-path'); |
30 | 48 |
|
31 | | - icon?.setAttribute('d', checkPath); |
| 49 | + // Show check icon to indicate successful copy |
| 50 | + icon?.setAttribute('d', checkIconPath); |
| 51 | + |
32 | 52 | if (code) { |
33 | 53 | copy(code); |
34 | 54 | } |
35 | 55 |
|
| 56 | + // Reset to copy icon after 2 seconds |
36 | 57 | setTimeout(() => { |
37 | | - icon?.setAttribute('d', copyPath); |
| 58 | + icon?.setAttribute('d', copyIconPath); |
38 | 59 | }, 2000); |
39 | 60 | }; |
40 | 61 |
|
41 | | - if (buttonProps) { |
42 | | - Object.entries(buttonProps || {}).forEach(([key, value]) => { |
43 | | - button.setAttribute(key, value); |
| 62 | + // Apply additional button properties |
| 63 | + if (rest) { |
| 64 | + Object.entries(rest).forEach(([key, value]) => { |
| 65 | + if (value !== undefined) { |
| 66 | + button.setAttribute(key, String(value)); |
| 67 | + } |
44 | 68 | }); |
45 | 69 | } |
46 | 70 |
|
47 | | - container?.appendChild(button); |
| 71 | + container.appendChild(button); |
48 | 72 | } |
49 | 73 |
|
50 | | -const MarkdownRenderer = React.forwardRef( |
| 74 | +/** |
| 75 | + * Renders markdown content with code blocks that have copy buttons |
| 76 | + */ |
| 77 | +const MarkdownRenderer = forwardRef<HTMLDivElement, MarkdownRendererProps>( |
51 | 78 | (props: MarkdownRendererProps, ref: React.Ref<HTMLDivElement>) => { |
52 | 79 | const { children, className, copyButtonProps, ...rest } = props; |
53 | | - const mdRef = React.useRef<HTMLDivElement>(null); |
| 80 | + const mdRef = useRef<HTMLDivElement>(null); |
54 | 81 |
|
55 | 82 | useEffect(() => { |
56 | | - mdRef.current?.querySelectorAll('.rcv-code-renderer').forEach((el: any) => { |
57 | | - appendCopyButton(el, copyButtonProps); |
| 83 | + // Add copy buttons to all code blocks |
| 84 | + const codeBlocks = mdRef.current?.querySelectorAll('.rcv-code-renderer'); |
| 85 | + codeBlocks?.forEach(codeBlock => { |
| 86 | + createCopyButton(codeBlock as HTMLDivElement, copyButtonProps); |
58 | 87 | }); |
| 88 | + // We only want to run this once when the component mounts |
59 | 89 | // eslint-disable-next-line react-hooks/exhaustive-deps |
60 | 90 | }, []); |
61 | 91 |
|
|
0 commit comments