目次

highlight.jsにコピー機能を追加する方法

シンタックス・ハイライト・ライブラリhighlight.jsに簡単にコピー機能を追加する方法をご紹介します。


PREPARATION


想定する読者と前提条件

基本的なパソコン操作ができること。HTML及びJavaScriptの基礎程度の知識が必要です。


START


highlight.jsのとは?

highlight.jsは、ブログ等でプログラムコードを掲載する場合、予約語などのキーワードを 色分けし、視認性を高めるライブラリです。今回はこのライブラリにコピー機能を追加し操作性を向上させてみましょう。 最終的には以下のようなものを作成いたします。

2022年3月3日現在最新バージョン:11.4.0です。

コードタイトル

HTML
<div class="row text-center mb-2 mt-5 justify-content-center">
    <div class="col-8 col-md-4 mx-auto">
        <h2 class="cairo c-r h3 d-inline-block">
            <img decoding="async" class="ai" alt="" src="/img/ai/ai2.png"><br>
            COMPLATE</h2>
        <hr class=" ai-border">
    </div>
</div>

highlight.jsの設置

下記公式サイトにアクセスし、最新の設置方法を確認してください。最も簡単な方法はcdnjsを利用します。

<link rel="stylesheet"
      href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/highlight.min.js"></script>

highlightjs公式サイト

見た目を作ろう

コードをハイライトするためには、pre要素、code要素が必要となります。今回は、その周りにコードタイトル、コード種別、コピーボタン等のパーツを追加してみましょう。

コピーボタンの追加

コピーボタン等を追加したHTMLは以下のとおりです。(簡単な構造ですね)

<div class="docs">
    <div class="control">
        <div>
            <p>コードタイトル</p>
        </div>
        <div>
            <span class="code-type">CODE TYPE</span>
            <button class="copy-button">COPY CODE</button>
        </div>
    </div>
    <pre><code class="">
    </code></pre>
</div>

見た目を整えよう

次に、CSSを追加しコピーボタン等の装飾をしていきましょう。

.docs{
    border:1px solid #cdcdcd;
}
.docs .control{
    display:flex;
    justify-content:space-between;
    padding:10px 0px;
}
.docs .control p{
    margin:6px 0 0 10px;
    padding:0;
    font-size:1rem;
    font-weight: bold;
    color:#212529;
}
.docs .control div:last-child{
    min-width:230px;
}
.docs pre{
    margin:0;
}
.code-type{
    background:#888;
    color:#fff;
    display:inline-block;
    padding:8px 16px;
    margin-left:10px;
    margin-right:8px;
    font-size:.7rem;
}
.copy-button{
    color:#888;
    font-size:.6rem;
    display:inline-block;
    padding:8px 16px;
    margin-right:20px;
    box-shadow:0px 2px 5px 0px rgba(0,0,0,0.16), 0px 2px 6px 0px rgba(0,0,0,0.12);
    border:2px solid #666;
    cursor: pointer;
    background-color: #fff;
    transition: all .2s ease
}
.copy-button.success{border-color:#00C851;background-color:#c8e6c9;color:#007E33;}
.copy-button.failed{border-color:#ff4444;background-color:#ffcdd2;color:#CC0000;}
.copy-button:hover{
    box-shadow:0px 2px 5px 0px rgba(0,0,0,0.3), 0px 2px 10px 0px rgba(0,0,0,0.12);
}
@media only screen and (max-width:768px) {
    .docs .control{
        flex-direction: column-reverse;
    }
    .docs .control div:last-child{
        text-align:right;
    }
}

コピー機能を作ろう

ここからが本題です。JavaScriptを使用してクリップボードへコピーする機能を作成します。 クリップボードへコピーする方法はいくつかありますが、今回はexecCommandを使用したコピー方法を取り上げます。

execCommandで実現できるクリップボードへのコピー機能は、ブラウザで選択されている文字列をクリップボードへコピーするという機能になります。 この機能を使用するためには、JavaScriptでクリップボードへコピーしたい文字列を選択させる必要があります。JavaScriptでは文字列の選択を制御するためには、window.getSelectionを使用することで文字列の選択範囲を操作することができます。

document.execCommand

クリップボードへのコピーコマンドを実行します。(実行可能な多くのコマンドは、文書の選択範囲に対して影響を及ぼします。今回は選択範囲の内容をクリップボードにコピーできるコマンド「copy」を使用しクリップボードを制御します。)

Document.execCommand

window.getSelection

この機能は、ユーザーが選択した文字列の範囲やキャレットの現在位置を示す Selection オブジェクトを返します。 document.getSelection() も同様の機能で違いはありません。

selectionオブジェクトには様々な機能がありますが、今回使用するものだけご紹介します。

getRangeAt

選択範囲の取得が行なえます。(選択されている1つないし複数の range(範囲) のうちの1つを表す range オブジェクトを返します。)

removeAllRanges

選択範囲をすべてリセットします。(全ての range(範囲) を selection から削除します。)

addRange

選択範囲を追加します。(selection に range(範囲) オブジェクトを追加します。)

window.getSelection

selectionオブジェクト

document.createRange

rangeオブジェクトは、documentの断片(範囲)を表します。

selectNodeContents

範囲を決定します。(Rangeをある Node(DOM)の内容を含むように設定します。)

コピープログラム完成版

/*
 * querySelector等で取得した要素(element)を渡すと、
 * その要素の文字列をクリップボードにコピーします。
 * ユーザーが事前に選択していた内容には影響を与えません。
 * 戻り値はコピーが成功したかどうか(true/false)
*/
let copyToClipboard = (element) => {
    let ranges = [];
    let selection = window.getSelection();
    let range = document.createRange();
    let result = false;
    for(let i = 0; i < selection.rangeCount; i += 1) {
        ranges[i] = selection.getRangeAt(i);
    }
    range.selectNodeContents(element);
    selection.removeAllRanges();
    selection.addRange(range);
    result = document.execCommand("copy");
    selection.removeAllRanges();
    for(let i = 0; i < ranges.length; i += 1) {
        selection.addRange(ranges[i]);
    }
    return result;
};

コピーボタンに割り当てよう

コピーボタンにイベントの割当を行いましょう。クラス名(.copy-button)を持つ要素に対してaddEventListenerを用いてイベントの割当を行います。 ポイントは、closestを使用し親要素の探索を行い、そこからpre(コード部分)を見つけ出します。

Highlightjs TEST(1)

let els = document.querySelectorAll(".copy-button");
els.forEach((el) => {
    el.addEventListener("click",(e) => {
        try{
            const doc = el.closest(".docs");
            const pre = doc.querySelector("pre");
            if(pre){
                copyToClipboard(pre);
            }
        } catch (e) {
            //error 
        }
    });
});


QUESTION


  • avatar
    ちょっと待ってくれ、コピーできてるのかい?

    画面上変化がないとコピーできたかどうか不安なんだけど。

  • コピーボタンを押したときに変化を付けてみましょう。

    コピーが成功した場合、失敗した場合でコピーボタンの色、テキストを変更するコードを追加してみましょう。

    avatar

コピーによる変化バージョン

let els = document.querySelectorAll(".copy-button");
let active = [];
els.forEach((el) => {
    el.addEventListener("click",(e) => {
        try{
            const doc = el.closest(".docs");
            const pre = doc.querySelector("pre");
            let result = false;
            if(active.indexOf(el) !== -1) return;
            if(pre){
                result = copyToClipboard(pre);
                el.innerText = (result ? "COPIED!" : "FAILED!");
                el.classList.add((result ? "success" : "failed"));
                active.push(el);
                setTimeout(() => {
                    let index =  active.indexOf(el);
                    el.className = "copy-button";
                    el.innerText = "COPY CODE";
                    if(index !== -1)active.splice(index, 1);
                },2000);
            }
        } catch (e) {
            //error 
        }
    });
});
  • avatar
    機能の追加も簡単だね。

    やっぱり画面上の変化があったほうが伝わりやすいな。プログラムもそんなに増えていないね。

  • 実装自体も簡単でしょう?

    少量のコードでユーザーの利便性が向上したわね。思い通りの機能を作るのは楽しい。下記ページで動作確認ができるわ。

    Highlightjs TEST(2)

    avatar
  • avatar
    コピー機能だけ追加することはできるかい?

    毎回タイトルやコード名、ボタンのコードを書くのが大変なんだけど、コピーボタンだけ簡単に追加する方法はあるかい?

  • 簡単にできるわよ?

    highlight.jsを少しだけ拡張して、自動的にコピーボタンが表示されるようにしてみましょう。こうすると毎回ボタンのコードを書く手間が減るわね。 好きな方を使って構わないわ。

    avatar

自動でコピーボタン追加


/*
 * これは変更ありません。
*/
let copyToClipboard = (element) => {
    let ranges = [];
    let selection = window.getSelection();
    let range = document.createRange();
    let result = false;
    for(let i = 0; i < selection.rangeCount; i += 1) {
        ranges[i] = selection.getRangeAt(i);
    }
    range.selectNodeContents(element);
    selection.removeAllRanges();
    selection.addRange(range);
    result = document.execCommand("copy");
    selection.removeAllRanges();
    for(let i = 0; i < ranges.length; i += 1) {
        selection.addRange(ranges[i]);
    }
    return result;
};
/* ボタン位置が異なるため、少し手直し! bodyからイベントを拾うように変更*/
let body = document.querySelector("body");
let active = [];
body.addEventListener("click",(e) => {
    try{
        const target = e.target;
        if(!target.classList.contains('copy-button'))return;
        
        const pre = target.closest("pre");
        let result = false;
        if(active.indexOf(target) !== -1) return;
        if(pre){
            result = copyToClipboard(pre);
            target.innerText = (result ? "COPIED!" : "FAILED!");
            target.classList.add((result ? "success" : "failed"));
            active.push(target);
            setTimeout(() => {
                let index =  active.indexOf(target);
                target.className = "copy-button";
                target.innerText = "COPY CODE";
                if(index !== -1)active.splice(index, 1);
            },2000);
        }
    } catch (e) {
        //error 
    }
});
/* highlightを拡張しコードブロックに自動的にボタンを追加する */
hljs.addPlugin({
    'after:highlightElement': ({ el, result }) => {
      el.innerHTML = `<button class="copy-button">COPY CODE</button>${result.value}`;
    }
});

CSSによる装飾

pre{
    position:relative;
}
.copy-button{
    color:#888;
    font-size:.6rem;
    display:inline-block;
    padding:8px 16px;
    margin-right:20px;
    box-shadow:0px 2px 5px 0px rgba(0,0,0,0.16), 0px 2px 6px 0px rgba(0,0,0,0.12);
    border:2px solid #666;
    cursor: pointer;
    background-color: #fff;
    transition: all .2s ease;
    position:absolute;
    right:0;
    top:-20px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.copy-button.success{border-color:#00C851;background-color:#c8e6c9;color:#007E33;}
.copy-button.failed{border-color:#ff4444;background-color:#ffcdd2;color:#CC0000;}
.copy-button:hover{
    box-shadow:0px 2px 5px 0px rgba(0,0,0,0.3), 0px 2px 10px 0px rgba(0,0,0,0.12);
}
  • avatar
    ほとんどかわらないな~!

    ボタンの追加がこんなに簡単にできるなんて。簡易的なものなら十分だね!

  • addPluginのおかげね。

    強調表示への整形が終わった後のデータを変更できるのはとても素晴らしいわ。たった5行でボタンの追加が済んだもの。

    Highlightjs TEST(3)

    avatar


COMPLETE


  • avatar
    お疲れ様でした!

    今回は見た目もシンプルなコピー機能の実装をしてみました。コピーした場合の動作等は簡単なコードで追加できますので、色々工夫してみてください。 あ、行番号の記事も書きました。よかったら見てみてね。この機能との組み合わせも楽よ?


関連記事



コメント