mizusame

コードブロックにコピーボタンを実装する(Clipboard API 対応版)

ブログやサイトなどでのコードブロック(ソースコード表示)に、highlight.jsGoogle Code PrettifyPrism などのシンタックスハイライターを用いたり、素の pre 要素と code 要素を使っている方は多いと思います。

そのようなコードブロックにコピーボタンが付いていると、読者の方がすぐにコードを利用できてとても便利です。

しかしインターネット上に公開されているコピーボタンのソースコードは、すでに廃止されている execCommand('copy') を未だに使用しているものが多く、いつ使えなくなるかわかりません。

そこで今回は、execCommand('copy') の代替案である Clipboard API に対応したソースコードのコピーボタンを作成しました。

アイキャッチ

コピーボタンの実装方法

前提として、当記事で公開するコピーボタンは以下のように pre 要素と code 要素を用いたコードブロックを対象にしています。

<pre><code>
  <!-- ソースコード -->
</code></pre>

pre 要素のみ、code 要素のみのコードブロックには対応しておりませんので、予めご了承ください。

コピーボタンのソースコードは、以下に示すように「コピーボタン作成用コード」と「コピー機能用コード」の2つのブロックに分かれています。

<script>
// コピーボタン作成用コード
(function(d){
  const pre = d.querySelectorAll('pre');
  if(pre.length == 0){
    return
  }
  for(let i = 0; i < pre.length; i++){
    const btn = d.createElement('button');
    btn.className = 'copy-button';
    btn.textContent = 'Copy';
    pre[i].insertBefore(btn, pre[i].firstElementChild);
    btn.addEventListener('click', copy_code, false)
  }
})(document);

// コピー機能用コード

</script>

コピーボタン作成用コードは全コピーボタン共通です。

次項で紹介するコピー機能用コードは、各ボタンによってコードが異なります。

使いたいボタンのコードを上記ソースコードの「// コピー機能用コード」の直下に追加してください。

スクリプトの設置場所は </body> の上あたり(できれば直前) がおすすめです。

Blogger でコピーボタンを使う場合は、コードの <script><script>//<![CDATA[ に、</script>//]]></script> にそれぞれ置き換えてください。

コードブロックおよびコピーボタンの CSS はこちらを参考にしてください。

pre{
  position: relative;
  margin: 2em 0;
  padding: 0;
  line-height: 1;
  font-size: 15px;
  background: #ddd;
}
pre code{
  display: block;
  margin: 0;
  padding: 2em 1em 1em;
  max-height: 240px;
  line-height: 1.4;
  overflow: auto;
}
.copy-button{
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  appearance: none;
  -moz-appearance: none;
  -webkit-appearance: none;
  margin: 0 auto;
  padding: 0 1em;
  height: 24px;
  border: none;
  line-height: 1;
  font-weight: bold;
  background: #666;
  color: #fff;
  cursor: pointer;
}
.copy-button:hover{
  background: #999
}

コピーボタン各種

コードコピー後の挙動を変えて 4種類のコピーボタンを作ってみました。

それぞれ、if(navigator.clipboard) で使っているブラウザの Clipboard API 対応・非対応を、then ... catch でコピーの成功・失敗を振り分けています。

その1:アラート表示

コピーが完了すると、「コピーしました」とアラートが表示されます。

function copy_code(){
  const code = this.nextElementSibling.innerText;
  if(navigator.clipboard){
    navigator.clipboard.writeText(code).then(() => {
      alert('コピーしました')
    }).catch(e => {
      alert('コピーできませんでした\nお手数ですが手動でコピーしてください\n\n' + e.message)
    })
  } else{
    alert('このブラウザは Clipboard API 非対応です\nお手数ですが手動でコピーしてください')
  }
}

コピーするたびにアラートが表示されるので、それを消す作業が少し面倒かもしれません……。

その2:ボタンテキスト変化

コピーが完了すると、コピーボタンのテキストが「Copy」から「Copied!!!」に変わり、0.5秒(500ミリ秒)後に元の表記に戻ります。

function copy_code(){
  const code = this.nextElementSibling.innerText;
  if(navigator.clipboard){
    navigator.clipboard.writeText(code).then(() => {
      this.textContent = 'Copied!!!';
      setTimeout(() => {
        this.textContent = 'Copy'
      }, 500)
    }).catch(e => {
      alert('コピーできませんでした\nお手数ですが手動でコピーしてください\n\n' + e.message)
    })
  } else{
    alert('このブラウザは Clipboard API 非対応です\nお手数ですが手動でコピーしてください')
  }
}

「Copy」から「Copied」だとパッと見変化がわかりづらいかなと思ったので感嘆符(!)を 3つ付けてみました。

その3:背景色変化

コピーが完了すると、コードブロックの背景色が変わり、0.5秒(500ミリ秒)後に元の背景色に戻ります。

function copy_code(){
  const code = this.nextElementSibling.innerText;
  if(navigator.clipboard){
    navigator.clipboard.writeText(code).then(() => {
      this.parentNode.style.background = '#666';
      setTimeout(() => {
        this.parentNode.style.background = ''
      }, 500)
    }).catch(e => {
      alert('コピーできませんでした\nお手数ですが手動でコピーしてください\n\n' + e.message)
    })
  } else{
    alert('このブラウザは Clipboard API 非対応です\nお手数ですが手動でコピーしてください')
  }
}

highlight.js の行番号表示プラグイン highlightjs-line-numbers.js をお使いの場合、このコピーボタンだとコードブロックに不具合が生じる可能性があります。

以前似たようなコピーボタンを公開したときに不具合の報告を頂いたのですが、筆者の環境だと現象の再現ができず、現時点では修正ができません。

もし不具合が起きた場合は、他のコピーボタンをお使いいただけると助かります。

その4:選択解除

コードブロックが全選択され、コピーが完了すると 0.5秒(500ミリ秒)後に選択が解除されます。

function copy_code(){
  const code = this.nextElementSibling;
  window.getSelection().selectAllChildren(code);
  if(navigator.clipboard){
    navigator.clipboard.writeText(code.innerText).then(() => {
      setTimeout(() => {
        window.getSelection().removeAllRanges()
      }, 500)
    }).catch(e => {
      alert('コピーできませんでした\nお手数ですが選択されている部分を手動でコピーしてください\n\n' + e.message)
    })
  } else{
    alert('このブラウザは Clipboard API 非対応です\nお手数ですが選択されている部分を手動でコピーしてください')
  }
}

ソースコードが全選択されているので、Clipboard API が動かない場合でもコンテキストメニューやキーボードの操作でコピーを容易に行なえるのが、このボタンのメリットです。

あとがき

Clipboard API を用いたソースコードのコピーボタンを作成しました。

Internet Explorer のサポートが 2022年6月15日に終了したおかげで、IE を考慮する必要がなくなって助かりました。まだ使えるとはいえ、すでに廃止されている execCommand('copy') を使うのは気が進まなかったので……。

調子に乗って 4パターンも作ってしまいましたが、また別のコピーボタンのアイデアが浮かんだらこっそり追加しておこうと思います。

この記事がどなたかの参考になれば幸いです。

    編集
    ホーム