上一篇我們讓程式碼區塊顯示了標題,這篇我們繼續讓它更好用,來加入「複製按鈕」!
結果截圖如下:
我的個人網站裡也有此系列的好讀版,程式碼更易讀、也支援深色模式和側邊目錄,歡迎前往閱讀!
我們將使用 MDX 客製化元件的方法來加入複製按鈕。
Markdown 在渲染程式碼區塊時,會渲染成 <pre>
元素。
我們可以新增自己的 <CustomPre/>
React 元件,並稍做設定,來讓 MDX 渲染程式碼區塊時,改用我們提供的 <CustomPre/>
來渲染。
具體實作方法是參考這篇文章:How to add a copy code button to your blog posts - Phil Stainer。
<CustomPre/>
新增 /src/components/CustomPre.tsx
:
// ref: https://philstainer.io/blog/copy-code-button-markdown
import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react';
import { copyToClipboard } from '@/lib/copyToClipboard';
import { removeDuplicateNewLine } from '@/lib/removeDuplicateNewLine';
type Props = React.ComponentPropsWithoutRef<'pre'>;
function CustomPre({ children, className, ...props }: Props) {
const preRef = useRef<HTMLPreElement>(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timer);
}, [copied]);
const onClick = async () => {
if (preRef.current?.innerText) {
await copyToClipboard(removeDuplicateNewLine(preRef.current.innerText));
setCopied(true);
}
};
return (
<div className="group relative">
<pre
{...props}
ref={preRef}
className={clsx(className, 'focus:outline-none')}
>
<div className="absolute top-0 right-0 m-2 flex items-center rounded-md bg-[#282a36] dark:bg-[#262626]">
<span
className={clsx('hidden px-2 text-xs text-green-400 ease-in', {
'group-hover:flex': copied,
})}
>
已複製!
</span>
<button
type="button"
aria-label="Copy to Clipboard"
onClick={onClick}
disabled={copied}
className={clsx(
'hidden rounded-md border bg-transparent p-2 transition ease-in focus:outline-none group-hover:flex',
{
'border-green-400': copied,
'border-gray-600 hover:border-gray-400 focus:ring-4 focus:ring-gray-200/50 dark:border-gray-700 dark:hover:border-gray-400':
!copied,
}
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={clsx('pointer-events-none h-4 w-4', {
'text-gray-400 dark:text-gray-400': !copied,
'text-green-400': copied,
})}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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"
className={clsx({ block: !copied, hidden: copied })}
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
className={clsx({ block: copied, hidden: !copied })}
/>
</svg>
</button>
</div>
{children}
</pre>
</div>
);
}
export default CustomPre;
新增 /src/lib/copyToClipboard.ts
:
// ref: https://philstainer.io/blog/copy-code-button-markdown
export const 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(void 0);
} catch (e) {
reject(e);
}
}
});
};
新增 /src/lib/removeDuplicateNewLine.ts
:
// Workaround to work with rehype-prism-plus generated Pre blog for copy to clipboard feature
export const removeDuplicateNewLine = (text: string): string => {
if (!text) return text;
return text
.replace(/(\r\n\r\n)/gm, `\r\n`)
.replace(/(\n\n)/gm, `\n`)
.replace(/(\r\r)/gm, `\r`);
};
<CustomPre/>
替換掉文章內文程式碼區塊新增 /src/lib/mdxComponents.ts
:
import CustomPre from '@/components/CustomPre';
// Custom components/renderers to pass to MDX.
const mdxComponents = {
pre: CustomPre,
};
export default mdxComponents;
修改 /src/pages/posts/[slug].tsx
,import mdxComponents 傳給 <MDXContent>
:
import type { GetStaticPaths, GetStaticProps, NextPage } from 'next';
// ...
// 新增下面這行,import mdxComponents
import mdxComponents from '@/lib/mdxComponents';
// ...
const PostPage: NextPage<Props> = ({ post, prevPost, nextPost }) => {
// ...
const MDXContent = useMDXComponent(code);
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="icon" href="/favicon.ico" />
</Head>
<PostLayout post={post} prevPost={prevPost} nextPost={nextPost}>
// 修改下面這行,把 mdxComponents 傳給 MDXContent
<MDXContent components={mdxComponents} />
</PostLayout>
</>
);
};
export default PostPage;
完成了!使用 pnpm dev
並進入含有程式碼區塊的文章,就會看到程式碼區塊多出複製按鈕了!
http://localhost:3000/posts/post-with-code
結果截圖如下:
恭喜你成功加入「複製按鈕」到程式碼區塊了。
這是我們最後一篇部落格樣式調整了,目前它已經相當好看了。
下一篇讓我們來使用 next-seo,為全站加入 Open Graph、meta data 等 SEO 優化手法!