- 記事一覧 >
- ブログ記事
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等でご連絡ください。