Closed mustafaabobakr closed 2 years ago
Yes, just modify pre
or code
in your MDX components
prop and add some custom code.
@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>
</>
);
};
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.
Thank you! That works 🎉
Better add this to docs 👀 Thanks
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)
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'];
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?
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.
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.
@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 />
}
prism-code
offers acopy-to-clipboard
button that can be styled, Can this be done !?