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 +