rehype-pretty / rehype-pretty-code

Beautiful code blocks for Markdown or MDX.
https://rehype-pretty.pages.dev
MIT License
1.06k stars 64 forks source link

[Feature] Copy To Clipboard button #34

Closed mustafaabobakr closed 2 years ago

mustafaabobakr commented 2 years ago

prism-code offers a copy-to-clipboard button that can be styled, Can this be done !?

image

atomiks commented 2 years ago

Yes, just modify pre or code in your MDX components prop and add some custom code.

mustafaabobakr commented 2 years ago

@atomiks

How to use button inside pre code ? I tried this approach but failed

<CopyToClipboad />
interface CodeBlockProps {
  language?: SupportedLang;
  children: string;
};
const CodeBlock: React.FC<CodeBlockProps> = ({ language, children }) => {
  const codeRef = useRef(null);
  useEffect(() => {
    if (codeRef && codeRef.current) {
      hljs.highlightElement(codeRef.current);
    }
  }, [codeRef]);

  return (
    <>
      <pre>
        <code ref={codeRef} className={`language-${language}`}>{children}</code>
      </pre>
    </>
  );
};
atomiks commented 2 years ago

You'd do smth like this:

const components = {
  pre(props) {
    return <pre {...props}>
      <button onClick={copy}>Copy</button>
      {props.children}
    </pre>;
  }
}

Every <pre> tag will have that button.

mustafaabobakr commented 2 years ago

Thank you! That works 🎉

mustafaabobakr commented 2 years ago

Better add this to docs 👀 Thanks

mustafaabobakr commented 2 years ago

Done

https://www.youtube.com/watch?v=dOLtm2Ep6kE

image

image

AnishDe12020 commented 2 years ago

You'd do smth like this:

const components = {
  pre(props) {
    return <pre {...props}>
      <button onClick={copy}>Copy</button>
      {props.children}
    </pre>;
  }
}

Every \<pre\> tag will have that button.

I was trying this approach but how do I like to get the content to copy to the clipboard (that is, the code)? Here is what my MDXComponents.tsx file looks like -

import { MDXComponents } from "mdx/types";
import Link from "@/components/Shared/Link";

const CodeBlock = ({ children, ...otherProps }) => {
  console.log(children);

  return (
    <pre {...otherProps}>
      <button>Copy</button>
      {children}
    </pre>
  );
};

const CustomMDXComponents: MDXComponents = {
  a: Link,
  pre: CodeBlock,
};
export default CustomMDXComponents;

I am using Contentlayer (which uses MDX Bundler)

mustafaabobakr commented 2 years ago
const codeRef = useRef<HTMLElement>(null);

const components = {
  Img: Image_dynamic,
  Link: CustomLink,
  pre(props) {
    return (
      <div style={{ position: "relative", overflow: "auto" }}>
        <CopyToClipboard elementToCopyRef={codeRef}></CopyToClipboard>
        <pre {...props} >
        {cloneElement(props.children as React.ReactElement, { ref: codeRef })}
        </pre>
      </div>
    );
  }
} as React.ComponentProps<typeof MDXProvider>['components'];
AnishDe12020 commented 2 years ago
const codeRef = useRef<HTMLElement>(null);

const components = {
  Img: Image_dynamic,
  Link: CustomLink,
  pre(props) {
    return (
      <div style={{ position: "relative", overflow: "auto" }}>
        <CopyToClipboard elementToCopyRef={codeRef}></CopyToClipboard>
        <pre {...props} >
        {cloneElement(props.children as React.ReactElement, { ref: codeRef })}
        </pre>
      </div>
    );
  }
} as React.ComponentProps<typeof MDXProvider>['components'];

Ok so, I got the ref part, but that doesn't have the raw code right?

khinshankhan commented 1 year ago

Heyy, just implemented this. So the example is off a bit, the useRef should be within the pre function since it'll be a unique ref per pre instance. You can then use ctx.current?.textContent to get the content as text from the ref. Mine looks a bit like this:

import type { MDXComponents } from "mdx/types";
import type { RefObject, DetailedHTMLProps, HTMLAttributes, ReactElement } from "react";
import { useState, useEffect, useRef, cloneElement } from "react";

const CopyToClipboardButton = ({ ctx }: { ctx: RefObject<HTMLElement> }) => {
  const [clicked, setClick] = useState(false);
  useEffect(() => {
    const timer = setTimeout(() => {
      setClick(false);
    }, 3000);
    return () => clearTimeout(timer);
  }, [clicked, setClick]);

  async function onClick() {
    // TODO: account for potential errors?
    await navigator.clipboard.writeText(ctx.current?.textContent ?? "Failed to copy");
    setClick(true);
  }

  const text = clicked ? "Copied!" : "Click to copy code";
  return (
    <button aria-label={text} onClick={onClick} style={{ float: "right" }}>
      {text}
    </button>
  );
};

const Pre = (props: DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement>) => {
  const codeRef = useRef<HTMLElement>(null);

  return (
    <pre {...props}>
      <CopyToClipboardButton ctx={codeRef} />
      {cloneElement(props.children as ReactElement, { ref: codeRef })}
    </pre>
  );
};

const components: MDXComponents = {
  pre: Pre,
};

I kept running into dumb linting issues if I used hooks in a non-capitalized component (hence Pre), typescript life :weary: You could remove the types if you're not using typescript :shrug:

I'm not done yet so it's a bit rough, but basic copy functionality works, so this should be enough to get going. Just gotta style away at the button. I'd recommend perhaps using an IconButton with svg icons + tooltips.

Thanks for the slick use of cloneElement.

chrism commented 1 year ago

Thanks for the great library, I'm sharing this code for anyone else that is using Next.js 13 with server components and wants to add a copy code button.

The button must be a client component because of the onClick handler.

'use client'

const CodeCopyButton = ({ code }: { code: string }) => {
  const copyCode = () => {
    navigator.clipboard.writeText(code)
  }

  return <button onClick={copyCode}>Copy code</button>
}

export default CodeCopyButton

Combined with a custom pre component in mdx-components.tsx as suggested in this comment

import type { MDXComponents } from 'mdx/types'
import CodeCopyButton from '@/components/CodeCopyButton'

import Children from 'react-children-utilities'

const pre = ({ children, ...props }: any) => {
  const code = Children.onlyText(children)

  return (
    <pre {...props}>
      <CodeCopyButton code={code} />
      {children}
    </pre>
  )
}

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    pre,
    ...components,
  }
}

Note: this uses React Children Utilities to generate a text string from the children property.

Hope this might be useful to someone.

mtr1990 commented 1 year ago

@chrism Perfect thanks!

BTW, Is there a way to enter dynamically in Mdx file?

Next.js just provide such static example:

export default async function Page() {

const HelloWorld = await import( './hello.mdx')

  return <HelloWorld />
}

When i try dynamic input according to Params it doesn't work

export default async function Page({ params }) {

const HelloWorld = await import( './${params.id}.mdx')

  return <HelloWorld />
}