エンジニアの皆様なら日々お世話になっているであろうコピー&ペースト。
普段何気なく使っていますが、例えばVSCodeからExcelにコピペすると、テキストだけでなくシンタックスハイライトまでペーストされたりして、よく考えると少し不思議な動作をしています。
本記事では、私がJavaScriptのClipboard APIを試行錯誤した記録とともに、
こういったコピペの内部動作についてや、JavaScriptにおけるクリップボード制御のあれこれについてまとめようと思います。
以下、Clipboard APIを試行錯誤した記録が続きます。
タイトルの「Excelへいい感じにコピペできるWebページ」だけ知りたい方は 「Excelへいい感じにコピペできるWebページ」を作ってみる の章へどうぞ。
Clipboard APIについて
JavaScriptのClipboard APIは、ブラウザでコピペ周りの操作を行う時にお世話になる機能です。
クリップボードの中身を参照したり、コピー・カット・ペーストといった操作をされたときの処理を独自にカスタマイズしたりできます。
例えばペースト時にテキストの中身を取得したい場合、 ClipboardEvent.clipboardData の getData() メソッド を利用して以下のように作るのが一番簡単です。
// ペーストイベントを登録したいdom要素
const input = document.getElementByID("input")
input.addEventListener("paste", (event) => {
// clipboardDataプロパティを取得
const clipboardData = event.clipboardData
// 例えば以下のようにすると、クリップボードにあるテキストを取得できる
console.log(clipboardData.getData("text"))
});
ペーストされるデータの中身を見てみる
上記のようなプログラムを組みつつ、デバッグツールで動作を追ってみて気づいたのですが、どうやら ClipboardEvent.clipboardData の中にはリスト形式でいくつものデータが格納されている様子。
これはリファレンスでも説明されており、 items というプロパティで参照できるようです。
このデータ一覧を見てみたいと思い、以下のような感じで ClipboardEvent.clipboardData が持つリスト形式のデータを全て取得するプログラムを組みました。
window.addEventListener("load",()=>{
const input = document.getElementById("input")
input.addEventListener("paste", (event) => {
// clipboardDataプロパティを取得
const clipboardData = event.clipboardData
// デフォルト動作は解除しておく
event.preventDefault()
if(clipboardData.items){
for (const item of clipboardData.items) {
if (item.kind === "string") {
// kindが"string"なら、getAsStringメソッドで文字列形式のデータを取得できる
item.getAsString((s) => {
console.log(`kind = ${item.kind}, type = ${item.type}, string = ${s}`)
})
}
else if (item.kind === "file") {
// kindが"file"の場合、getAsFileメソッドでファイルオブジェクトが取得できる
const f = item.getAsFile()
// ファイルアクセス用の一時URLを作って表示する
const url = this.window.URL.createObjectURL(f)
console.log(`kind = ${item.kind}, type = ${item.type}, url = ${url}`)
// 一時URLの削除
this.window.URL.revokeObjectURL(url)
}
}
}
})
})
これをcodepen上で実行可能にしたのが以下になります。
流石に console.log では味気ないので簡単にDOMへ表示するようにしつつ、画像などのファイルも可能な限り表示するようにもしました。
See the Pen clipboardData.items一覧 by takematsu132 (@takematsu132) on CodePen.
試しに、 Qiitaの「2026年!初アウトプットをしよう」 から以下の部分をコピーし、上のプログラムにペーストしてみます。(Mac版のGoogle Chromeを利用)
↓
2種類のデータが出てきました。
1つは text/plain ということでプレーンテキスト。メモ帳とか、ブラウザの検索窓とかにペーストするとこれが出てくるので、こちらは見慣れたデータですね。
もう1つは text/html 。普通にコピペしていると見ることがないデータですが、なるほどhtml形式のデータもクリップボード上に保持していると。
どうやら選択範囲のhtmlソースをそのままクリップボード持っているようです。ローカルにhtmlを作って表示してみると、CSSまでは含まれてないようでスタイルは当たりませんが、構造はコピー元と同じになっています。
以上より、ブラウザでWebページのテキストをコピーしたとき、内部的には text/plain と text/html という2種類のデータを保持していることがわかりました。
普段はプレーンテキストの方しか目にしませんが、その裏ではhtml形式のデータまで持っているんですね。
こんな感じで、いくつかのアプリからこのプログラムにペーストして挙動を確認してみました。
ブラウザ(画像)
ブラウザはブラウザでも、画像右クリック→「画像をコピー」した結果をペーストするとまた違った結果が出てきます。
-
text/html- イメージタグのhtmlデータ:画像のurlはフルパスになる模様
-
image/png- 画像のデータそのもの
text/plain が無いためか、検索窓とかにペーストしても何も出てきません。一方 image/png があることで、ペイントソフトに画像データをペーストすることが可能になっています。
Visual Studio Code
-
text/plain- 普通にプレーンテキスト
-
text/html- htmlデータ:ブラウザと異なり、コードハイライト等のスタイルが埋め込まれている
-
application/vnd.code.copymetadata、vscode-editor-data- id等が含まれている、VSCodeの内部情報?
- 矩形選択をしたらその情報が入ったり、コピー元のファイル情報(
.jsからコピーしたらmode:javascriptなど)が出ていたりする
基本はブラウザに近いデータですが、追加で内部データを持たせているようです。
Excel、Word
どちらも同じようなデータを持っていました。以下手元にあった2019Mac版の結果です。
-
text/plain、text/html- この辺はVSCodeやブラウザと同じ
-
text/htmlの方はしっかり<html>や<head>といったタグが含まれていて、よりhtmlらしい感じに
-
image/png- コピーした範囲の画像データ
- 確かにExcelやWordでは、「図の貼り付け」を用いることで画像としてペーストできますが、まさかコピー時に画像データをクリップボードにセットしていたとは
-
text/rtf-
rtfということは、リッチテキスト?ただし、私の方で確認した限り中身は空のようです
-
エクスプローラー(Windows) / Finder(Mac)
pdfだったら application/pdf 、 htmlだったら text/html …など、コピーしたfileのオブジェクトを持っている様子。
ClipboardEvent.clipboardData の中身は、ドラッグ&ドロップでも用いられる DataTransfer であり、エクスプローラー等からブラウザへファイルをドラッグ&ドロップしたときと同じ様な挙動をしているように見えます。
Numbers(iPhone)
少し志向を変えて、iPhoneのクリップボードを確認すべくNumbersからペーストしたところ、ブラウザ等と同様 text/plain と text/htmlを持っていることを確認。
iPhoneのアプリであっても、ほぼ同じようなデータを持たせているようです。
コピーされるデータを改変してみる
あくまでJavaScriptのClipboard APIで取得できる範疇ではありますが、アプリによってクリップボード上にさまざまな情報を持たせていることがわかりました。
傾向としては、「 text/plain text/html は持ちつつ、その他にアプリ独自データが配置されている」みたいなパターンが多いようです。
となると、冒頭で記載した「VSCodeからExcelにコピペすると、テキストだけでなくシンタックスハイライトまでペーストされる」動作については、Excelがクリップボード上の text/html を解釈しているのではないか?と推測できるとともに、
「 クリップボードの text/html を改変すれば、Excel等へペーストされるデザインを独自で変更できるのではないか? 」
という仮説が浮かびます。
これを確かめるべく、今度はコピー時にクリップボード内のデータを改変するプログラムを作成します。そのためには、以下の Clipboard.write() を利用するとよさそうです。
JavaScriptにおける、生データを扱うオブジェクトである Blob を渡せばいいとのこと。
これを踏まえて、まずは以下のようにクリップボードへの書き込み関数を作成。
/**
* @param {Object.<string,{text:string}|{blob:Blob}>} write_data
* クリップボードへ書き込むデータ\
* keyに「text/plain」「image/png」等のデータタイプ(MIMEタイプ)を指定、\
* valueに書き込むデータとして、"text"か"blob"を指定する
* @returns clipboardの「write」結果(Promise<void>)
*/
async function writeClipbpard(write_data){
// まずは[key,blob]のturple配列に変換
const clipboard_entries = Object.entries(write_data).map(([key,value])=>{
if(value["text"]){ // "text"が渡ってきたら自前でBlobを作る
const blob=new Blob([value["text"]],{type:key})
return [key,blob]
}
else if(value["blob"]){ // "blob"が来たらBlobが入ってると見なしてそのまま
return [key,value["blob"]]
}
else{ // それ以外が来たら一応空文字をセットしておく
return [key,""]
}
})
// 上のturple配列をオブジェクトにしつつ、ClipboardItemを作る
const clipboard_item = [new ClipboardItem(Object.fromEntries(clipboard_entries))]
// clipboardにwriteする
return navigator.clipboard.write(clipboard_item)
}
あとは、「コピー」ボタンが押されたときなど、任意のタイミングでこの関数を呼び出すようにプログラムを組みます。
window.addEventListener("load",()=>{
// 今回は「コピー」ボタンが押されたときとする
const button = document.getElementById("copy_button")
// まあまあ新しいブラウザが要求されるので、非対応ブラウザははじいておく
if(typeof ClipboardItem === 'undefined'){
document.getElementById("msg").innerText = "非対応ブラウザです"
button.disabled=true
return
}
// ボタン押下時、クリップボードにデータを登録するイベントを作成
button.addEventListener("click",async ()=>{
// text/htmlにセットするデータ
// ※<!--StartFragment-->はおまじない(無いとダメっぽい?)
const html=`
<!--StartFragment-->
<table>
<tr><td style="color:red;font-size:16px;border:1px solid black;background:yellow;width:300px;">HelloWorld!</td></tr>
</table>
<!--EndFragment-->
`
try{
// クリップボードへの書き込み関数をコール
await writeClipbpard({
"text/html":{text:html},
"text/plain":{text:"Hello World!"} // "text/plain"は適当にセット
})
document.getElementById("msg").innerText = "コピー成功"
}catch(_e){
// 書き込み失敗
document.getElementById("msg").innerText = "クリップボードへの書き込みエラー"
}
})
})
これをcodepen上で実行可能にしたのが以下になります。
See the Pen コピーテスト by takematsu132 (@takematsu132) on CodePen.
text/html には、適当に黄背景に赤字で「HelloWorld!」という表示をするhtmlデータをセット。
Excelからコピーしたときにクリップボードへ配置される text/html を参考に、<table>タグを利用しています。
上記codepen上のプログラムにある「コピーする」ボタンを押すことで、クリップボードにデータが配置されます。
text/plain 側には"Hello World!"とテキストを入れており、ブラウザの検索窓やメモ帳等にペーストしたらこれが表示されると思います。
一方、これを手元にあったExcel2019(Mac版)へペーストすると以下のようになりました。
黄背景に赤字の「HelloWorld!」。背景色やボーダーまで貼り付けられていますね。ただし、width が反映されないなど、何もかもが再現できるわけではないようです。
仮説通り、 クリップボード上の text/html データを改変することで、Excelへのペーストをカスタマイズすることができました。
Excel2019(Mac版)のみ動作を確認しています
もしかしたらExcelのバージョン等によっては、違う動作をするかもしれません
クリップボードに画像をセットする
さて、上のcodepenのJavaScriptソースをご覧になるとわかると思いますが、 実はこっそりクリップボードに画像データを配置していました。
ペイントソフトや、Excel上で「形式を選択してペースト」→「図の貼り付け」を選択してペーストすると、画像データが貼り付けられると思います。
ということで、クリップボードにはテキストに限らず画像を持たせることが可能です。ソースは以下のようになります。
window.addEventListener("load",async ()=>{
/* ~中略~ */
// クリップボードに配置したいpng画像を、Base64エンコードした文字列を用意し、fetch関数に渡す
const img_fetch = await fetch("data:image/png;base64,AAAAIGZ0eXBhd...")
// fetch結果からblobを生成
const img_blob = await img_fetch.blob()
// ボタン押下時、クリップボードにデータを登録するイベントを作成
button.addEventListener("click",async ()=>{
try{
// クリップボードへの書き込み関数をコール
await writeClipbpard({
"text/html":{text:html},
"text/plain":{text:"Hello World!"}, // "text/plain"は適当にセット
"image/png":{blob:img_blob}
})
document.getElementById("msg").innerText = "コピー成功"
}catch(_e){
// 書き込み失敗
document.getElementById("msg").innerText = "クリップボードへの書き込みエラー"
}
})
})
ここで一つ注意点なのですが、 navigator.clipboard.write を呼び出すイベント…今回で言えば button の click イベント内では、 navigator.clipboard.write を呼び出すまでの間に await 等の非同期処理を書いてはいけない ようです。というより、書いてしまうとiPhone等でコピーに失敗してしまいます。この点、以下のブログ記事で解説があります。
これを踏まえ、 fetch や blob といった非同期処理は、ボタンのイベントの外に配置しています。
クリップボードに隠しデータをセットする
クリップボードへデータをセットするもう一つのやり方として、 clipboardData.setData を用いる方法があります。
こちらを用いる場合、私の方で試した限りは テキストデータのみセット可能で、画像をセットすることはできませんでした。
その代わり、 text/plain や text/html といったデータ型について、MIMEタイプに限らず任意の型を指定できる ようです。つまり以下のようにすることで、クリップボードに独自のデータをセットできます。
window.addEventListener("load",()=>{
// コピー時のイベント
window.addEventListener('copy',(event) => {
// デフォルト動作は解除しとく必要あり
event.preventDefault()
// text/plainには、選択されている箇所の文字を入れておく
event.clipboardData.setData("text/plain",window.getSelection().toString())
// たとえばformatに「隠しデータ」を指定
event.clipboardData.setData("隠しデータ","見えない…ってコト!?")
})
// ペースト時のイベント
const input = document.getElementById("input")
input.addEventListener("paste", (event) => {
// デフォルト動作は解除しとく必要あり
event.preventDefault()
// clipboardDataプロパティを取得
const clipboardData = event.clipboardData
// このinputにペーストされたときだけ、「隠しデータ」を参照する
const value = event.clipboardData.getData("隠しデータ")
input.value = value
})
})
codepenで再現したのが以下になります。
See the Pen Untitled by takematsu132 (@takematsu132) on CodePen.
このhtml上でコピーしたとき、メモ帳等に貼り付ける限りは普通のコピペと同じように見える動作をします。
ただし、ソースに記載の通り、その裏では「隠しデータ」をクリップボードに配置しています。
このデータは、上記codepenの入力枠にてペーストしたときのみ表示されるデータとなるわけです。
「Excelへいい感じにコピペできるWebページ」を作ってみる
遅くなりましたがタイトルの回収です。これまでの結果を活用すれば、
- Excelへコテコテにデザインしたhtmlをペーストできる
- 更には上記htmlの画像版もペーストできる
みたいな機能を有した、「Excelへ良い感じにコピペできるWebページ」が作れるのでは?と思ったのでやってみます。
今回は簡単な実験として、Excelで「ペースト後のデザイン」を作成→そこからクリップボードにセットするデータを作る流れで行ってみます。
クリップボード上の text/html をExcel等の表計算ソフトが解釈するのは公式が言ってる話ではないので、以下の動作は今後変わっていく可能性があります。
なんならExcelのバージョン等によっては、現時点でもうまく動作しないかもしれません。以降の記載は、Mac版のExcel2019でのみ動作を確認しています。
まず「ペースト後のデザイン」として、以下のようにExcelでコードブロックを作成してみました。
これは、Qiitaの記事からコードブロックを1行ごとコピペして、背景色を黒にしただけのものです。
そしたら、このコードブロックを選択してコピーし、クリップボードから text/html のデータを取り出します。
以下、クリップボード内のデータ一覧を取得するプログラムを再掲します。
See the Pen clipboardData.items一覧 by takematsu132 (@takematsu132) on CodePen.
Excelがクリップボードに登録するhtmlデータは結構量がありますが、内部情報のようなデータも多く、デザインを再現するうえでは <style> タグの部分と <table> タグの部分だけあれば良いようです。
この二つを合体させて、まずは以下のように表示用のhtmlファイルを作りました。コメントにも記載していますが、JavaScriptで <style> タグと <table> タグのdomを取得したいので、それぞれidを振っておきます。
<!DOCTYPE HTML>
<html>
<head>
<meta charset=utf-8>
<title>コピペ</title>
<script type="text/javascript" src="js/script.js"></script>
<!--styleタグの部分 ここから-->
<!--id="style"を追加する-->
<style id="style">
<!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
@page
/* ...〜中略〜... */
-->
</style>
<!--styleタグの部分 ここまで-->
</head>
<body>
<!--コピー用のボタン-->
<input type="button" value="コピーする" id="copy_button">
<p id="msg"></p>
<!--tableタグの部分 ここから-->
<!--id="target"を追加する-->
<table border=0 cellpadding=0 cellspacing=0 width=761 style='border-collapse:
collapse;width:571pt' id="target">
<!--StartFragment-->
<col width=761 style='mso-width-source:userset;mso-width-alt:20882;width:571pt'>
<!-- ...〜中略〜 ...-->
</tr>
<!--EndFragment-->
</table>
<!--tableタグの部分 ここまで-->
</body>
</html>
続いてJavaScriptを作成します。
コピーされるデータを改変してみる のものをほぼそのままですが、htmlデータはdom上から取得するようにしています。
/**
* @param {Object.<string,{text:string}|{blob:Blob}>} write_data
* クリップボードへ書き込むデータ\
* keyに「text/plain」「image/png」等のデータタイプを指定、\
* valueに書き込むデータとして、"text"か"blob"を指定する
* @returns clipboardの「write」結果(Promise<void>)
*/
async function writeClipbpard(write_data){
// まずは[key,blob]のturple配列に変換
const clipboard_entries = Object.entries(write_data).map(([key,value])=>{
if(value["text"]){ // "text"が渡ってきたら自前でBlobを作る
const blob=new Blob([value["text"]],{type:key})
return [key,blob]
}
else if(value["blob"]){ // "blob"が来たらBlobが入ってると見なしてそのまま
return [key,value["blob"]]
}
else{ // それ以外が来たら一応空文字をセットしておく
return [key,""]
}
})
// 上のturple配列をオブジェクトにしつつ、ClipboardItemを作る
const clipboard_item = [new ClipboardItem(Object.fromEntries(clipboard_entries))]
// clipboardにwriteする
return navigator.clipboard.write(clipboard_item)
}
window.addEventListener("load",async ()=>{
const button = document.getElementById("copy_button")
if(typeof ClipboardItem === 'undefined'){
document.getElementById("msg").innerText = "非対応ブラウザです"
button.disabled=true
return
}
const target = document.getElementById("target")
// htmlはdomから取得する → styleとtableのhtml文字列を取得し、合体させれば良い
const html = document.getElementById("style").outerHTML + target.outerHTML
button.addEventListener("click",async ()=>{
try{
await writeClipbpard({
"text/html":{text:html},
"text/plain":{text:target.innerText}
})
document.getElementById("msg").innerText = "コピー成功"
}catch(_e){
document.getElementById("msg").innerText = "クリップボードへの書き込みエラー"
}
})
})
ここまでで、「表示用のhtml」と「そのhtmlを text/html としてクリップボードへセットする」処理が出来ました。次は画像をクリップボードにセットする処理を作ります。
今回は、domから画像データを作成できる「html2canvas」というライブラリを用いて、クリップボードに画像をセットする処理を作ります。
まずはhtmlにてhtml2canvasへの参照を追加。
<!DOCTYPE HTML>
<html>
<head>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<!--以下略-->
そしたらJavaScript側に処理を追加。 クリップボードに画像をセットする ではbase64でエンコードしたpng画像からBlobを作りましたが、こちらではcanvasからBlobを作成します。
// 前略
const target = document.getElementById("target")
// htmlはdomから取得する → styleとtableのhtml文字列を取得し、合体させれば良い
const html = document.getElementById("style").outerHTML + target.outerHTML
+ // html2canvasを使って、テーブルの画像を作成する
+ // 非同期処理なのでボタンのクリックイベント外にかく
+ const canvas = await html2canvas(target)
+ const img_promise = await fetch(canvas.toDataURL("image/png"))
+ const img_blob = await img_promise.blob()
button.addEventListener("click",async ()=>{
try{
await writeClipbpard({
"text/html":{text:html},
"text/plain":{text:target.innerText},
+ "image/png":{blob:img_blob}
})
document.getElementById("msg").innerText = "コピー成功"
}catch(_e){
document.getElementById("msg").innerText = "クリップボードへの書き込みエラー"
}
これをcodepenで再現したのが以下になります。
See the Pen Untitled by takematsu132 (@takematsu132) on CodePen.
「コピーする」ボタンを押してExcelにペーストすると以下のようになります。いい感じにコピペできているのではないでしょうか。
ちなみに、GoogleスプレッドシートやiPhoneのNumbersでもペーストができます。また、 image/png を持たせている事でペイントソフトにもペースト可能です。
ただしhtmlについては、以下のようにアプリによって微妙に解釈のされ方が異なるようで、過信してはいけなさそうですね。
まとめ
- 一口に「コピペ」といっても、プレーンテキスト以外に様々な情報をやり取りしており、画像やhtmlデータ、その他独自情報を持たせるようアプリごとに制御している
- この制御はJavaScriptでもそれなりに可能であり、クリップボードに画像や隠しデータを持たせたり、Excel向けにデザインを調整したりといったことが可能
本記事ではJavaScriptにおけるクリップボード制御の一例として、「Excelへいい感じにコピペできるWebページ」を記載しました。
書いといて何ですが、正直Excelを見越したコピペ処理をWebページで作りたい事なんてあまりないと思いますし、本記事の内容をそのまま使うことは無いかもしれませんが、
クリップボード制御はExcel以外にも様々なアプリで行われていますし、応用すれば面白い事が出来そうです。
余談: navigator.clipboard を使ったクリップボードデータの参照と権限について
クリップボードデータの参照については ClipboardEvent.clipboardData の getData() メソッド を利用する方法を記載してきました。これが一番簡単ではあるのですが、ユーザーが「ペースト」操作をしたとき…つまりはCtrl+Vとか、右クリック→貼り付けとかの操作をしたときにしか参照できません。
コピーでやった時のように、ボタンクリックしたときとかにクリップボードの中身を参照したい場合、以下の navigator.clipboard.read() や navigator.clipboard.readText() を使えば可能ではあります。
window.addEventListener("load",async()=>{
const button = document.getElementById("paste_button")
// 「ペーストボタン」が押されたときに、クリップボードのテキストを取得して画面表示する
button.addEventListener("click",async()=>{
navigator.clipboard
.readText()
.then((clipText) => (document.getElementById("msg").innerText = clipText));
})
})
これをcodepenに実装したのが以下です。
See the Pen navigator.clipboard paste by takematsu132 (@takematsu132) on CodePen.
Chromeで「ペーストする」ボタンを押すと以下のようなアクセス許可が求められます。(環境によって動作は変わるかもしれません)
冷静に考えれば、クリップボードはパスワードなどの機密情報を持ってることもあるわけです。上のcodepenは画面表示するだけでしたが、裏密かにサーバーとかに送られてしまってはたまったもんじゃありません。
なので、Ctrl+Vや、右クリック→貼り付けといった「ユーザーが明確にペースト操作をしたとき」以外でクリップボードの中身を見ようとすると、「クリップボードの中身が見られようとしてるけど大丈夫?」と確認が入るようです。
…となると気になるのは、「じゃぁクリップボードへの書き込み処理である navigator.clipboard.write() とかは権限いらないの?」という所。これまでの「コピーする」ボタンは特にアクセス許可を求められなかったと思います。
この点、記事中で参照した以下のブログ記事にて、こんな事が書かれていました。
ユーザー操作がトリガーの場合は、
clipboard-read権限が付与されていなくても書き込みが可能です。
…つまりこう、「ボタンを押した」みたいなユーザーの操作をトリガーに navigator.clipboard.write() を呼ぶ分には、権限が要らないということでしょうか?この点は、正直私もよくわかっていません。
Excelとかでも同じようなことをやっているとはいえ、「ブラウザ上で」「こっそり」画像等のデータをクリップボードに持たせられるというのは、個人的には漠然とした怖さといいますか、具体的には思いつかないもののすごく悪用できそうなにおいを感じます。





