highlight.jsにコピー機能を追加する方法
シンタックス・ハイライト・ライブラリhighlight.jsに簡単にコピー機能を追加する方法をご紹介します。
更新:
環境:windows10/Microsoft Edge/VS Code
PREPARATION
想定する読者と前提条件
基本的なパソコン操作ができること。HTML及びJavaScriptの基礎程度の知識が必要です。
START
highlight.jsのとは?
highlight.jsは、ブログ等でプログラムコードを掲載する場合、予約語などのキーワードを 色分けし、視認性を高めるライブラリです。今回はこのライブラリにコピー機能を追加し操作性を向上させてみましょう。 最終的には以下のようなものを作成いたします。
2022年3月3日現在最新バージョン:11.4.0です。
コードタイトル
<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>
見た目を作ろう
コードをハイライトするためには、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」を使用しクリップボードを制御します。)
window.getSelection
この機能は、ユーザーが選択した文字列の範囲やキャレットの現在位置を示す Selection オブジェクトを返します。 document.getSelection() も同様の機能で違いはありません。
selectionオブジェクトには様々な機能がありますが、今回使用するものだけご紹介します。
- getRangeAt
-
選択範囲の取得が行なえます。(選択されている1つないし複数の range(範囲) のうちの1つを表す range オブジェクトを返します。)
- removeAllRanges
-
選択範囲をすべてリセットします。(全ての range(範囲) を selection から削除します。)
- addRange
-
選択範囲を追加します。(selection に range(範囲) オブジェクトを追加します。)
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(コード部分)を見つけ出します。
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
コピーによる変化バージョン
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
}
});
});
自動でコピーボタン追加
/*
* これは変更ありません。
*/
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);
}