What is the easiest or recommended method to add a copy code button? #1948
Replies: 8 comments 11 replies
-
There’s currently not one cookie-cut strategy to get copy buttons on code. The possibilities you have are explained in Extending MDX. The MDX site uses React Server Components (RSC), which are not ready for use yet. The site also has a custom plugin to add those buttons: Line 207 in aff6de4 |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
you can use i do the same thing except i have a separate component that looks like: import clsx from 'clsx'
import React from 'react'
interface ICopyToClipboard {
children: React.ReactChild
}
export const CopyToClipboard = ({ children }: ICopyToClipboard) => {
const textInput = React.useRef<HTMLDivElement>(null)
const [hovered, setHovered] = React.useState(false)
const [copied, setCopied] = React.useState(false)
const onEnter = () => {
setHovered(true)
}
const onExit = () => {
setHovered(false)
setCopied(false)
}
const onCopy = () => {
setCopied(true)
if (textInput.current !== null && textInput.current.textContent !== null)
navigator.clipboard.writeText(textInput.current.textContent)
setTimeout(() => {
setCopied(false)
}, 2000)
}
return (
<div
ref={textInput}
onMouseEnter={onEnter}
onMouseLeave={onExit}
className="relative code-block"
>
{hovered && (
<button
aria-label="Copy code"
type="button"
className={clsx(
'absolute right-2 top-2 w-8 h-8 p-1 rounded border-2 bg-gray-700 dark:bg-gray-800',
{
'focus:outline-none focus:border-green-400 border-green-400': copied,
'hover:border-gray-300': !copied,
}
)}
onClick={onCopy}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
className={copied ? 'text-green-400' : 'text-gray-300'}
>
{copied ? (
<>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</>
) : (
<>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</>
)}
</svg>
</button>
)}
{children}
</div>
)
} and then i wrap my import React from 'react'
import { CopyToClipboard } from '@/components/index'
interface IPre {
children: React.ReactElement
theme: string
showLineNumbers: boolean
}
export const Pre = ({ children, theme, showLineNumbers, ...props }: IPre) => {
return (
<CopyToClipboard>
<pre
className={`px-4 py-3 overflow-x-auto rounded-lg font-jetbrains ${
theme ? `${theme}-theme` : 'bg-syntaxBg'
} ${showLineNumbers ? 'line-numbers' : ''}`}
>
{children}
</pre>
</CopyToClipboard>
)
} this works well for me :) |
Beta Was this translation helpful? Give feedback.
-
I have now tried to work with the components attribute as described in the documentation but nothing happens. Where could be the error? My import { fileURLToPath, URL } from 'url';
import { h, s } from 'hastscript';
import { visit } from 'unist-util-visit';
import { toText } from 'hast-util-to-text';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import mdx from '@mdx-js/rollup';
import { babel } from '@rollup/plugin-babel';
import remarkGfm from 'remark-gfm';
import remarkFrontmatter from 'remark-frontmatter';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeSlug from 'rehype-slug';
import rehypeHighlight from 'rehype-highlight';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
mdx({
jsx: true,
remarkPlugins: [remarkGfm, remarkFrontmatter],
rehypePlugins: [
rehypeHighlight,
rehypeSlug,
[
rehypeAutolinkHeadings,
{
behavior: 'prepend',
properties: {
ariaLabel: 'Link to this section',
className: ['anchor']
},
content: link()
}
]
]
}),
babel({
extensions: ['.js', '.jsx', '.cjs', '.mjs', '.md', '.mdx'],
plugins: ['@vue/babel-plugin-jsx']
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/assets/scss/style.scss";`
}
}
}
});
function link() {
return s(
'svg.icon',
{
ariaHidden: 'true',
viewBox: [0, 0, 16, 16],
focusable: false,
width: 18,
height: 18
},
s('path', {
fill: 'grey',
d: 'M7.775 3.275C7.64252 3.41717 7.57039 3.60522 7.57382 3.79952C7.57725 3.99382 7.65596 4.1792 7.79337 4.31662C7.93079 4.45403 8.11617 4.53274 8.31047 4.53617C8.50477 4.5396 8.69282 4.46748 8.835 4.335L10.085 3.085C10.2708 2.89918 10.4914 2.75177 10.7342 2.65121C10.977 2.55064 11.2372 2.49888 11.5 2.49888C11.7628 2.49888 12.023 2.55064 12.2658 2.65121C12.5086 2.75177 12.7292 2.89918 12.915 3.085C13.1008 3.27082 13.2482 3.49142 13.3488 3.7342C13.4493 3.97699 13.5011 4.23721 13.5011 4.5C13.5011 4.76279 13.4493 5.023 13.3488 5.26579C13.2482 5.50857 13.1008 5.72917 12.915 5.915L10.415 8.415C10.2292 8.60095 10.0087 8.74847 9.76588 8.84911C9.52308 8.94976 9.26283 9.00157 9 9.00157C8.73716 9.00157 8.47691 8.94976 8.23411 8.84911C7.99132 8.74847 7.77074 8.60095 7.585 8.415C7.44282 8.28252 7.25477 8.21039 7.06047 8.21382C6.86617 8.21725 6.68079 8.29596 6.54337 8.43337C6.40596 8.57079 6.32725 8.75617 6.32382 8.95047C6.32039 9.14477 6.39252 9.33282 6.525 9.475C6.85001 9.80004 7.23586 10.0579 7.66052 10.2338C8.08518 10.4097 8.54034 10.5002 9 10.5002C9.45965 10.5002 9.91481 10.4097 10.3395 10.2338C10.7641 10.0579 11.15 9.80004 11.475 9.475L13.975 6.975C14.6314 6.31858 15.0002 5.4283 15.0002 4.5C15.0002 3.57169 14.6314 2.68141 13.975 2.025C13.3186 1.36858 12.4283 0.999817 11.5 0.999817C10.5717 0.999817 9.68141 1.36858 9.02499 2.025L7.775 3.275ZM3.085 12.915C2.89904 12.7292 2.75152 12.5087 2.65088 12.2659C2.55023 12.0231 2.49842 11.7628 2.49842 11.5C2.49842 11.2372 2.55023 10.9769 2.65088 10.7341C2.75152 10.4913 2.89904 10.2707 3.085 10.085L5.585 7.585C5.77074 7.39904 5.99132 7.25152 6.23411 7.15088C6.47691 7.05023 6.73716 6.99842 7 6.99842C7.26283 6.99842 7.52308 7.05023 7.76588 7.15088C8.00867 7.25152 8.22925 7.39904 8.415 7.585C8.55717 7.71748 8.74522 7.7896 8.93952 7.78617C9.13382 7.78274 9.3192 7.70403 9.45662 7.56662C9.59403 7.4292 9.67274 7.24382 9.67617 7.04952C9.6796 6.85522 9.60748 6.66717 9.475 6.525C9.14999 6.19995 8.76413 5.94211 8.33947 5.7662C7.91481 5.59029 7.45965 5.49974 7 5.49974C6.54034 5.49974 6.08518 5.59029 5.66052 5.7662C5.23586 5.94211 4.85001 6.19995 4.525 6.525L2.025 9.02499C1.36858 9.68141 0.999817 10.5717 0.999817 11.5C0.999817 12.4283 1.36858 13.3186 2.025 13.975C2.68141 14.6314 3.57169 15.0002 4.5 15.0002C5.4283 15.0002 6.31858 14.6314 6.975 13.975L8.225 12.725C8.35748 12.5828 8.4296 12.3948 8.42617 12.2005C8.42274 12.0062 8.34403 11.8208 8.20662 11.6834C8.0692 11.546 7.88382 11.4672 7.68952 11.4638C7.49522 11.4604 7.30717 11.5325 7.165 11.665L5.915 12.915C5.72925 13.1009 5.50867 13.2485 5.26588 13.3491C5.02308 13.4498 4.76283 13.5016 4.5 13.5016C4.23716 13.5016 3.97691 13.4498 3.73411 13.3491C3.49132 13.2485 3.27074 13.1009 3.085 12.915Z'
})
);
} My ---
title: Buttons
---
import BackifyButton from '@/components/Button.vue';
import Heading from './Heading.vue';
:::container
### Variants
The primary `Button` contains an animation effect.
**bold text**
... My <template>
<div class="view">
<!-- if I replace the 'Heading' string with the imported component, nothing happens -->
<MDXProvider v-bind:components="{ strong: 'Heading' }">
<component :is="mdx"></component>
</MDXProvider>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, computed } from 'vue';
import { useRoute } from 'vue-router';
import { MDXProvider } from '@mdx-js/vue';
import Heading from '@/views/Heading.vue';
const route = useRoute();
const mdx = computed(() => {
const routeName = route.name!.toString().replace(/\s/g, '');
return defineAsyncComponent(() => import('./' + routeName + '.mdx'));
});
</script> My <template>
<h1 style="color: red; size: 20px"><slot /></h1>
</template> I have also not found any example project that uses the latest mdx with vue 3/vite and works with custom components. |
Beta Was this translation helpful? Give feedback.
-
You can check rehype-starry-night-copy-collapse |
Beta Was this translation helpful? Give feedback.
-
I think I find a tricky way to solve this problem online. Even though you have syntax highlight plugin like prism or highlightjs. Only if you can replace component with custom component you can copy it. Below is the export default function copyToClipboard(text: string) {
return new Promise((resolve, reject) => {
if (navigator?.clipboard) {
const cb = navigator.clipboard;
cb.writeText(text).then(resolve).catch(reject);
} else {
try {
const body = document.querySelector("body");
const textarea = document.createElement("textarea");
body?.appendChild(textarea);
textarea.value = text;
textarea.select();
document.execCommand("copy");
body?.removeChild(textarea);
resolve(0);
} catch (e) {
reject(e);
}
}
});
} import { useEffect, useRef, useState } from "react";
import { copyToClipboard } from "@/lib/utils";
export default function Pre(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLPreElement>, HTMLPreElement>) {
const preRef = useRef<HTMLPreElement>(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timer);
}, [copied]);
const handleClickCopy = async () => {
if (preRef.current?.innerText) {
copyToClipboard(preRef.current.innerText);
setCopied(true);
}
};
return (
<div className='relative group'>
<pre {...props} ref={preRef}>
<button
type='button'
disabled={copied}
onClick={handleClickCopy}
aria-label='Copy to Clipboard'
className='absolute space-x-2 top-0 right-0 m-2 hidden transition bg-transparent border rounded-md p-2 focus:outline-none group-hover:flex disabled:flex fade-in'
>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-4 w-4 pointer-events-none'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
{copied ? (
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M5 13l4 4L19 7' />
) : (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2'
/>
)}
</svg>
</button>
{props.children}
</pre>
</div>
);
} Here is result: I find this solution in below two articles: |
Beta Was this translation helpful? Give feedback.
-
A lazy approach is adding the button on component mount. Not ideal but usable. const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const pres = ref.current?.querySelectorAll("pre");
pres?.forEach((pre) => {
pre.classList.add("relative");
const btn = document.createElement("button");
btn.className =
"absolute right-0 top-0 bg-gray-700 text-white text-sm p-2 rounded-bl-md";
btn.innerText = "Copy";
btn.addEventListener("click", () => {
const code = pre.querySelector("code")?.innerText;
if (code) {
navigator.clipboard.writeText(code);
}
btn.innerText = "Copied!";
setTimeout(() => {
btn.innerText = "Copy";
}, 1000);
});
pre.appendChild(btn);
});
}, [code]);
// render your mdx component and wrap it with the ref here |
Beta Was this translation helpful? Give feedback.
-
This is my solution. I hope this helps for someone. The key is Please note that you embed this component closest to the code block. import { useState } from 'react'
import * as Icon from './icons'
const className = {
button: 'text-white/70 hover:text-white',
}
export const ClipboardButton = () => {
const [isClicked, setIsClicked] = useState(false)
return (
<>
<button
className={className.button}
onClick={e => {
const target = e.currentTarget
const pre = target.closest('pre')
if (pre === null) {
console.log('Code not found')
return
}
const code = pre.textContent as string
// Check if the browser supports the Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
// Save the text to the clipboard
navigator.clipboard
.writeText(code)
.then(() => {
setIsClicked(isClicked => !isClicked)
target.disabled = true
setTimeout(() => {
setIsClicked(isClicked => !isClicked)
target.disabled = false
}, 800)
})
.catch(error => {
console.error('Failed to save text to clipboard:', error)
})
} else {
console.error('Clipboard API is not supported')
}
}}
>
{isClicked ? <Icon.CheckLinearIcon /> : <Icon.CopyLinearIcon />}
</button>
</>
)
} |
Beta Was this translation helpful? Give feedback.
-
Hi, I was wondering what is the easiest or recommended method to add a copy code button, like in the mdx docs page or like in the github markdown view.
I came across to the prismjs copy to clipboard button plugin and gatsby-plugin-mdx.
Beta Was this translation helpful? Give feedback.
All reactions