- 記事一覧 >
- ブログ記事

Next.jsのSyntax Highlighter部分にファイル名表示、コピーボタン機能を追加
はじめに
当ブログは、コード部分について、 react-markdown と react-syntax-highlighter を使っています。
この2つの npm の組み合わせだけでは、ファイル名表示とコピー機能が実装できません。
自力で実装しないといけません。
ファイル名表示とコピー機能とは、以下の部分の事です。
今回は、ファイル名表示とコピー機能を自力で実装しましたので、どうやって実装したかについて、紹介していきます。
ソースコード全体は、以下にあります。
ファイル名表示
前提
まず、markdown で書かれたブログ本文を react-markdown のコンポーネントで解析します。
コード部分のマークダウンは、以下のように書いています。(注意:先頭に全角スペースを入れていますが、実際は有りません。)
```tsx:components/Markdown.tsx
export const Markdown = (props: { source: string }): ReactElement => {
return (
<ReactMarkdown
key="content"
plugins={[gfm]}
source={props.source}
renderers={{
code: Code,
heading: HeadingRenderer,
}}
escapeHtml={false}
/>
);
};
```<ReactMarkdown /> コンポーネントのrenderersオプションに、{code: Code} を渡すことにより、react-markdownは、コード部分(```~```)の html 化を <Code /> コンポ―ネントに任せます。
ここは、元々こうなっていて、変更していません。変更したのは、
<Code />コンポ―ネントのコードです。
変更点
ファイル名表示に対応した結果が以下です。
import React, { Fragment, FC } from 'react';
import { okaidia } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { escapeHtml } from '../libs/Functions';
interface Props {
language: string;
value: string;
}
const Code: FC<Props> = ({ language, value }) => {
const [lang, filename] = (language || '').split(':');
return (
<Fragment>
{filename && (
<div className="code-block-filename-container">
<span className="code-block-filename">{escapeHtml(filename)}</span>
</div>
)}
<SyntaxHighlighter
language={lang === '' ? 'javascript' : lang}
style={okaidia}>
{value}
</SyntaxHighlighter>
</Fragment>
);
};
export default Code;思っていたより、単純に済みました。
Step1
const [lang, filename] = (language || '').split(':');↑
language 部分は、``` 以降が渡されます。
例1:```typescript → language は、 typescript
例2:```typescript:filename → language は、 typescript:filename
これをコロン : の前後で分離しています。
(コロンが無い場合、filename は、undefined です。)
コロンで分離するというのは、マイルールです。
これで、filename が取り出せるようになりました。
Step2
{
filename && (
<div className="code-block-filename-container">
<span className="code-block-filename">{escapeHtml(filename)}</span>
</div>
);
}↑<SyntaxHighlighter /> コンポーネントのすぐ上に div タグを filename に値が有るときだけ表示しています。escapeHtml は、libs/Functions.ts に定義した HTML に都合の悪い文字(&<>")をエスケープする関数です。
const HTML_ESCAPE_TEST_RE = /[&<>"]/;
const HTML_ESCAPE_REPLACE_RE = /[&<>"]/g;
const HTML_REPLACEMENTS: { [key: string]: string } = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
};
function replaceUnsafeChar(ch: string): string {
return HTML_REPLACEMENTS[ch];
}
export function escapeHtml(str: string): string {
if (HTML_ESCAPE_TEST_RE.test(str)) {
return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar);
}
return str;
}Step3
あとは、追加した div タグに対して、css を定義しただけです。
.js-toc-content .code-block-filename-container .code-block-filename {
display: table;
max-width: 100%;
background: #334155;
color: #f8fafc;
font-size: 0.875rem;
line-height: 1.25rem;
border-radius: 4px 4px 0 0;
padding: 6px 12px 20px;
padding-top: 6px;
padding-right: 12px;
padding-bottom: 20px;
padding-left: 12px;
margin-bottom: -20px;
}ファイル名表示 diff
ファイル名表示部分の diff は、以下です。
コピーボタン
変更点
クリップボードにコピーする機能は、 react-copy-to-clipboard の <CopyToClipboard /> コンポーネントを使っています。
これにより、ギミック部分だけを実装すれば良くなりました。
ギミック部分とは、以下の2つの機能です。
・コード部分にマウスカーソルが載ると、コピーボタンを表示(逆にコード部分以外にカーソルが出ていくと、コピーボタンを非表示)
・コピーボタンクリック時、"Copied!" の表示と消去
全体のソースコードは、以下です。先ほど、登場した <Code /> コンポーネント、Code.tsx で完結しています。
import React, { Fragment, FC, useState } from 'react';
import { okaidia } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { escapeHtml } from '../libs/Functions';
import CopyToClipboard from 'react-copy-to-clipboard';
interface Props {
language: string;
value: string;
}
const Code: FC<Props> = ({ language, value }) => {
const [lang, filename] = (language || '').split(':');
const [showCopyToClipboard, setShowCopyToClipboard] = useState(false);
const [styleTooltip, setStyleTooltip] = useState({
opacity: '0',
visiblity: 'hidden',
});
const handleClick = () => {
setStyleTooltip({ opacity: '1', visiblity: 'visible' });
setTimeout(function () {
setStyleTooltip({ opacity: '0', visiblity: 'hidden' });
}, 3000);
};
return (
<Fragment>
{filename && (
<div className="code-block-filename-container">
<span className="code-block-filename">{escapeHtml(filename)}</span>
</div>
)}
<div
style={{ position: 'relative' }}
onMouseEnter={() => setShowCopyToClipboard(true)}
onMouseLeave={() => setShowCopyToClipboard(false)}>
<SyntaxHighlighter
language={lang === '' ? 'javascript' : lang}
style={okaidia}>
{value}
</SyntaxHighlighter>
{showCopyToClipboard && (
<div className="code-block-copy-button">
<div className="copied-tooltip" style={styleTooltip}>
Copied!
</div>
<CopyToClipboard text={value} onCopy={() => handleClick()}>
<svg
id="btnTarget"
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
</svg>
</CopyToClipboard>
</div>
)}
</div>
</Fragment>
);
};
export default Code;Step1
クリップボードへのコピー機能を実装します。import CopyToClipboard from 'react-copy-to-clipboard';
と
クリップボードへのコピー機能を得たいアイコンに <CopyToClipboard /> コンポーネントを被せれば完成です。
これにより、クリックしたら、クリップボードへコピーするボタンが完成します。
<CopyToClipboard text={value} onCopy={() => handleClick()}>
<svg ・・・/>
・・・
</svg>
</CopyToClipboard>text= に渡すのは、コピーしたい内容の文字列になります。これは、react-markdown から渡ってくるため、そのまま渡しています。
Step2
コード表示部分にカーソルが入るとコピーボタンを表示するようにします。
<div
style={{ position: 'relative' }}
onMouseEnter={() => setShowCopyToClipboard(true)}
onMouseLeave={() => setShowCopyToClipboard(false)}>
・・・(コード表示部分)・・・
</div>コード表示部分に <div> タグを被せて、
onMouseEnter(マウスカーソルが入ってくる) → showCopyToClipboard = true
onMouseLeave(マウスカーソルが出ていく) → showCopyToClipboard = false
としています。
showCopyToClipboard は、状態管理を行う React フックの useState() を使っています。
const [showCopyToClipboard, setShowCopyToClipboard] = useState(false);で、「状態管理を行います。変数名は showCopyToClipboard で、showCopyToClipboard を変更する関数は、setShowCopyToClipboard。showCopyToClipboard の初期値は、false。」という意味です。
{showCopyToClipboard && (
<div className="code-block-copy-button">
・・・の && ( で、showCopyToClipboard の true, false により、表示、非表示を切り替えています。
Step3
<CopyToClipboard text={value} onCopy={() => handleClick()}>の onCopy={() => handleClick()} により、クリップボードへのコピーが完了したら、"Copied!" を表示して、3秒後、消えるようにしています。handleClick() の実装は、以下です。
const [styleTooltip, setStyleTooltip] = useState({
opacity: '0',
visiblity: 'hidden',
});
const handleClick = () => {
setStyleTooltip({ opacity: '1', visiblity: 'visible' });
setTimeout(function () {
setStyleTooltip({ opacity: '0', visiblity: 'hidden' });
}, 3000);
};また状態管理を行う React フックの useState() を使っていますが、今度は、管理対象の値が true, false ではなく、連想配列(オブジェクト)になります。
{ opacity: '1', visiblity: 'visible' } : 表示
{ opacity: '0', visiblity: 'hidden' } : 非表示
と css の style を変数化して管理しています。
これを JSX の style= のところに渡すと、style が適用されます。
<div className="copied-tooltip" style={styleTooltip}>
Copied!
</div>なお、コピーボタンの位置、マウスポインタ―の指定や、"Copied!"の表示非表示がふんわり適用されるように以下のように CSS を追加しています。
.js-toc-content .code-block-copy-button {
position: absolute;
cursor: pointer;
color: #9ca3af;
top: 10px;
right: 16px;
}
.js-toc-content .code-block-copy-button .copied-tooltip {
cursor: default;
position: absolute;
top: 0px;
right: 26px;
padding: 0px 3px;
display: inline-block;
white-space: nowrap;
background: #374151;
color: #f9fafb;
border-radius: 3px;
transition: 0.3s ease-in;
}コピーボタン機能 diff
コピーボタン機能部分の diff は、以下です。
以上!

その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。










