TAKEO

TAKEO

コードブロックにコピーボタンを追加する next-mdx-remote

コードブロックにコピーボタンを追加する方法

next-mdx-remoteではコードブロックのコピーボタンが利用できません。コピーボタンが有った方がクールでいいですよね。本記事では下のようなコピーボタンを実装する方法について紹介します。

Copied!!
1. 右上にマウスを合わせるとコピーボタンがでます。
1. コピーボタンを押すと、ボタンのスタイルが変化します。

方法の概要

本記事では次のコードをベースにします。

components/codeblock.js
Copied!!
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";

const CodeBlock = ({ className, children = "" }) => {
  // コードブロックの各要素を設定
  const match = /language-(\w+)(:?.*)/.exec(className || "");
  const language = match && match[1] ? match[1] : "";
  const code = String(children).replace(/\n$/, "");
  return (
    <>
      <SyntaxHighlighter
        language={language}
        style={atomDark}
        className="code-block"
      >
        {code}
      </SyntaxHighlighter>
      <style jsx global>{`
        .code-block {
          border-radius: 0.3rem !important;
          padding: 1.5rem !important;
        }
      `}</style>
    </>
  );
};

export default CodeBlock;

方法

  1. モジュールのインポート

    Copied!!
    $ npm install --save react-copy-to-clipboard
    $ npm install react-icons --save
    
  2. ボタンを実装する。変更量が多いのでdiff表示しています。

    Copied!!
    import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
    import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
    +import { useState } from "react";
    +import CopyToClipboard from "react-copy-to-clipboard";
    +import { BiCheck, BiCopy } from "react-icons/bi";
    
    const CodeBlock = ({ className, children = "" }) => {
    +  // コピーボタンの処理
    +  const [isButtonActive, setIsButtonActive] = useState(false);
    +  const normalStyle = {
    +    opacity: 0,
    +    transition: "0.1s",
    +  };
    +  const activeStyle = {
    +    opacity: 1,
    +    transition: "0.1s",
    +  };
    +  const copyBtnStyle = isButtonActive ? activeStyle : normalStyle;
    +
    +  // コピー完了メッセージの処理
    +  const [isCopied, setCopied] = useState(false);
    +  const handleClick = () => {
    +    setCopied(true);
    +    setTimeout(() => {
    +      setCopied(false);
    +    }, 3000);
    +  };
    +  const copiedStyle = isCopied ? activeStyle : normalStyle;
    +
      // コードブロックの各要素を設定
      const match = /language-(\w+)(:?.*)/.exec(className || "");
      const language = match && match[1] ? match[1] : "";
      const code = String(children).replace(/\n$/, "");
      return (
        <>
    -      <SyntaxHighlighter
    -        language={language}
    -        style={atomDark}
    -        className="code-block"
    +      <div
    +        className="code-block-wrapper"
    +        onMouseEnter={() => setIsButtonActive(true)}
    +        onMouseLeave={() => setIsButtonActive(false)}
          >
    -        {code}
    -      </SyntaxHighlighter>
    +        <div className="copied-tooltip" style={copiedStyle}>
    +          Copied!!
    +        </div>
    +        <div className="copy-button" style={copyBtnStyle}>
    +          <CopyToClipboard text={code} onCopy={() => handleClick()}>
    +            {isCopied ? (
    +              <BiCheck color="grey" size={20} />
    +            ) : (
    +              <BiCopy color="grey" size={20} />
    +            )}
    +          </CopyToClipboard>
    +        </div>
    +        <SyntaxHighlighter
    +          language={language}
    +          style={atomDark}
    +          className="code-block"
    +        >
    +          {code}
    +        </SyntaxHighlighter>
    +      </div>
    +      <style jsx>{`
    +        .code-block-wrapper {
    +          font-size: 0.9rem;
    +          margin-bottom: 2rem;
    +          position: relative;
    +        }
    +        .copy-button {
    +          display: inline-block;
    +          position: absolute;
    +          top: 0.8rem;
    +          right: 0.8rem;
    +          background-color: rgba(50, 50, 50, 0.1);
    +          border: 1px solid grey;
    +          border-radius: 0.3rem;
    +          padding: 0.2rem;
    +        }
    +        .copy-button:hover {
    +          background-color: rgba(50, 50, 50, 0.9);
    +          cursor: pointer;
    +        }
    +        .copied-tooltip {
    +          position: absolute;
    +          top: 0.8rem;
    +          right: 3.2rem;
    +          color: white;
    +          background-color: rgba(50, 50, 50, 0.5);
    +          border-radius: 0.2rem;
    +          padding: 0.3rem;
    +        }
    +      `}</style>
          <style jsx global>{`
            .code-block {
              border-radius: 0.3rem !important;
    
  3. MDXRemoteのコンポーネントに追加する

    pages/hoge.js
    Copied!!
    import CodeBlock from '../../components/codeblock';
    
    const components = {
      code: (props) => (
        <CodeBlock {...props} />
      ),
    };
    
    export default function HogePage({ source }){
      return (
        <MDXRemote {...source} components={components}>
      )
    }
    

終わりに

本記事ではコードブロックにコピーボタンを実装する方法について紹介しました。カスタマイザビリティが高くていいですね。オリジナルのものを作りましょう!最後に完成形をのせておきます。

components/codeblock.js
Copied!!
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import { useState } from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { BiCheck, BiCopy } from "react-icons/bi";

const CodeBlock = ({ className, children = "" }) => {
  // コピーボタンの処理
  const [isButtonActive, setIsButtonActive] = useState(false);
  const normalStyle = {
    opacity: 0,
    transition: "0.1s",
  };
  const activeStyle = {
    opacity: 1,
    transition: "0.1s",
  };
  const copyBtnStyle = isButtonActive ? activeStyle : normalStyle;

  // コピー完了メッセージの処理
  const [isCopied, setCopied] = useState(false);
  const handleClick = () => {
    setCopied(true);
    setTimeout(() => {
      setCopied(false);
    }, 3000);
  };
  const copiedStyle = isCopied ? activeStyle : normalStyle;

  // コードブロックの各要素を設定
  const match = /language-(\w+)(:?.*)/.exec(className || "");
  const language = match && match[1] ? match[1] : "";
  const code = String(children).replace(/\n$/, "");
  return (
    <>
      <div
        className="code-block-wrapper"
        onMouseEnter={() => setIsButtonActive(true)}
        onMouseLeave={() => setIsButtonActive(false)}
      >
        <div className="copied-tooltip" style={copiedStyle}>
          Copied!!
        </div>
        <div className="copy-button" style={copyBtnStyle}>
          <CopyToClipboard text={code} onCopy={() => handleClick()}>
            {isCopied ? (
              <BiCheck color="grey" size={20} />
            ) : (
              <BiCopy color="grey" size={20} />
            )}
          </CopyToClipboard>
        </div>
        <SyntaxHighlighter
          language={language}
          style={atomDark}
          className="code-block"
        >
          {code}
        </SyntaxHighlighter>
      </div>
      <style jsx>{`
        .code-block-wrapper {
          font-size: 0.9rem;
          margin-bottom: 2rem;
          position: relative;
        }
        .copy-button {
          display: inline-block;
          position: absolute;
          top: 0.8rem;
          right: 0.8rem;
          background-color: rgba(50, 50, 50, 0.1);
          border: 1px solid grey;
          border-radius: 0.3rem;
          padding: 0.2rem;
        }
        .copy-button:hover {
          background-color: rgba(50, 50, 50, 0.9);
          cursor: pointer;
        }
        .copied-tooltip {
          position: absolute;
          top: 0.8rem;
          right: 3.2rem;
          color: white;
          background-color: rgba(50, 50, 50, 0.5);
          border-radius: 0.2rem;
          padding: 0.3rem;
        }
      `}</style>
      <style jsx global>{`
        .code-block {
          border-radius: 0.3rem !important;
          padding: 1.5rem !important;
        }
      `}</style>
    </>
  );
};

export default CodeBlock;

関連記事