diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 35c094a..c297935 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -10,16 +10,16 @@ on:
jobs:
deploy:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Setup node
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
node-version: 'lts/*'
diff --git a/__tests__/MarkdownRenderer-test.js b/__tests__/MarkdownRenderer-test.js
index 0a45171..7a3b973 100644
--- a/__tests__/MarkdownRenderer-test.js
+++ b/__tests__/MarkdownRenderer-test.js
@@ -60,7 +60,7 @@ describe('MarkdownRenderer', () => {
// Check if copy button is rendered with the correct class
const copyButton = container.querySelector('button[title="Copy code"]');
expect(copyButton).toBeInTheDocument();
- expect(copyButton).toHaveClass('btn-copy-code');
+ expect(copyButton).toHaveAttribute('data-type', 'copy');
});
it('applies custom copy button props', () => {
diff --git a/docs/index.tsx b/docs/index.tsx
index 4f00756..5e09579 100644
--- a/docs/index.tsx
+++ b/docs/index.tsx
@@ -30,6 +30,9 @@ const App = () => {
renderExtraFooter={() => {
return
Footer
;
}}
+ copyButtonProps={{
+ className: 'rs-btn-icon rs-btn-icon-circle rs-btn rs-btn-subtle rs-btn-xs'
+ }}
>
{example}
diff --git a/src/CopyCodeButton.tsx b/src/CopyCodeButton.tsx
index 2838149..6786ba6 100644
--- a/src/CopyCodeButton.tsx
+++ b/src/CopyCodeButton.tsx
@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import copy from 'copy-to-clipboard';
-import classNames from 'classnames';
import CopyIcon from './icons/Copy';
import CheckIcon from './icons/Check';
@@ -10,7 +9,7 @@ interface CopyCodeButtonProps extends React.ButtonHTMLAttributes
+
{copied ? : }
);
diff --git a/src/MarkdownRenderer.tsx b/src/MarkdownRenderer.tsx
index 3c4e6a0..b8b1206 100644
--- a/src/MarkdownRenderer.tsx
+++ b/src/MarkdownRenderer.tsx
@@ -1,61 +1,91 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useRef, forwardRef } from 'react';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import mergeRefs from './utils/mergeRefs';
-import { iconPath as copyPath, svgTpl } from './icons/Copy';
-import { iconPath as checkPath } from './icons/Check';
+import { iconPath as copyIconPath, svgTpl } from './icons/Copy';
+import { iconPath as checkIconPath } from './icons/Check';
interface MarkdownRendererProps extends React.HTMLAttributes {
+ /**
+ * Markdown content as HTML string
+ */
children?: string | null;
+ /**
+ * Props to be passed to the copy button
+ */
copyButtonProps?: React.HTMLAttributes;
}
-function appendCopyButton(
+/**
+ * Creates and appends a copy button to a code container
+ * @param container - The container element to append the copy button to
+ * @param buttonProps - Additional props to apply to the copy button
+ */
+function createCopyButton(
container?: HTMLDivElement | null,
buttonProps?: React.HTMLAttributes
-) {
- if (!container) {
+): void {
+ // If the container is null or the container already has a copy button, return
+ if (!container || container.querySelector('button[data-type="copy"]')) {
return;
}
+ const { className, ...rest } = buttonProps || {};
const button = document.createElement('button');
- button.className = 'btn-copy-code';
+ button.dataset['type'] = 'copy';
button.title = 'Copy code';
- button.innerHTML = svgTpl(copyPath);
+ button.setAttribute('aria-label', 'Copy code');
+ button.innerHTML = svgTpl(copyIconPath);
- button.onclick = e => {
+ if (className) {
+ button.className = className;
+ }
+
+ button.onclick = (e: MouseEvent) => {
e.preventDefault();
- const code = container?.querySelector('code')?.textContent;
+ const code = container.querySelector('code')?.textContent;
const icon = button.querySelector('.copy-icon-path');
- icon?.setAttribute('d', checkPath);
+ // Show check icon to indicate successful copy
+ icon?.setAttribute('d', checkIconPath);
+
if (code) {
copy(code);
}
+ // Reset to copy icon after 2 seconds
setTimeout(() => {
- icon?.setAttribute('d', copyPath);
+ icon?.setAttribute('d', copyIconPath);
}, 2000);
};
- if (buttonProps) {
- Object.entries(buttonProps || {}).forEach(([key, value]) => {
- button.setAttribute(key, value);
+ // Apply additional button properties
+ if (rest) {
+ Object.entries(rest).forEach(([key, value]) => {
+ if (value !== undefined) {
+ button.setAttribute(key, String(value));
+ }
});
}
- container?.appendChild(button);
+ container.appendChild(button);
}
-const MarkdownRenderer = React.forwardRef(
+/**
+ * Renders markdown content with code blocks that have copy buttons
+ */
+const MarkdownRenderer = forwardRef(
(props: MarkdownRendererProps, ref: React.Ref) => {
const { children, className, copyButtonProps, ...rest } = props;
- const mdRef = React.useRef(null);
+ const mdRef = useRef(null);
useEffect(() => {
- mdRef.current?.querySelectorAll('.rcv-code-renderer').forEach((el: any) => {
- appendCopyButton(el, copyButtonProps);
+ // Add copy buttons to all code blocks
+ const codeBlocks = mdRef.current?.querySelectorAll('.rcv-code-renderer');
+ codeBlocks?.forEach(codeBlock => {
+ createCopyButton(codeBlock as HTMLDivElement, copyButtonProps);
});
+ // We only want to run this once when the component mounts
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/src/Renderer.tsx b/src/Renderer.tsx
index 93982e2..3e88ad0 100644
--- a/src/Renderer.tsx
+++ b/src/Renderer.tsx
@@ -11,6 +11,23 @@ import { transform as transformCode, Options } from 'sucrase';
const React = require('react');
const ReactDOM = require('react-dom');
+interface EditorProps {
+ /** The className of the editor */
+ className?: string;
+
+ /** Add a prefix to the className of the buttons on the toolbar */
+ classPrefix?: string;
+
+ /** The className of the code button displayed on the toolbar */
+ buttonClassName?: string;
+
+ /** Customize the code icon on the toolbar */
+ icon?: React.ReactNode;
+
+ /** The properties of the show code button */
+ showCodeButtonProps?: React.HTMLAttributes;
+}
+
export interface RendererProps extends Omit, 'onChange'> {
/** Code editor theme, applied to CodeMirror */
theme?: 'light' | 'dark';
@@ -28,18 +45,7 @@ export interface RendererProps extends Omit, '
editable?: boolean;
/** Editor properties */
- editor?: {
- className?: string;
-
- /** Add a prefix to the className of the buttons on the toolbar */
- classPrefix?: string;
-
- /** The className of the code button displayed on the toolbar */
- buttonClassName?: string;
-
- /** Customize the code icon on the toolbar */
- icon?: React.ReactNode;
- };
+ editor?: EditorProps;
/**
* https://github.com/alangpierce/sucrase#transforms
@@ -97,6 +103,7 @@ const Renderer = React.forwardRef((props: RendererProps, ref: React.Ref
+