＃37　Chromeの拡張機能を作ってみた～プレーンテキストでペーストしたい

## はじめに  
Google chatを利用する中でペーストイベントに対して不便を感じ、Chrome拡張機能で解決できないか試してみたので、その備忘録を前後半に分けてまとめていきたいと思います。  
記事の後半にあたる今回は、実際に作成したcontent.jsの中身について解説していきます。  
（前半の記事は[こちら](https://qiita.com/nxted_sapporo/items/afae4ecec10c3612c2fc)から閲覧いただけます）

## 拡張機能で実現したかったこと
Google chatで既に投稿されているチャットの一部をコピーして入力フォームにペーストすると、ペーストした文言の前に2行分の改行が挿入される仕様となっていました。
さらに、ペーストした箇所より上に入力値がない場合は送信するとこの2行分の改行は反映されませんが、ある場合はそのまま改行が反映された状態で投稿されてしまいます。
<br>
＜改行が挿入されてしまう場合のイメージ＞
![スクリーンショット #37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843084/be5db439-9e4f-b1f1-611d-7b0f12611272.png)
この仕様が、チャットでやりとりする中でコピペを多用する私にとっては不便に感じるものでした。
※こちらの仕様が確認できたのは2023/09/20時点で、2023/11/27の時点ではこの限りではないようです。
<br>
そのため、ペーストイベントが検出された際に改行が挿入されないように、拡張機能を利用して制御してみることにしました。

## ディレクトリ構成
今回作成した拡張機能のディレクトリ構成は以下の通りです。
[前回記事](https://qiita.com/nxted_sapporo/items/afae4ecec10c3612c2fc)でも触れましたが、用意しなければならないファイルは最低2つなので、簡単な拡張機能であれば、難しい構成ではないことがおわかりいただけるかと思います。
```json
gchatpaste   //任意のフォルダ名
├ content.js  //任意のファイル名
└ manifest.json
```



## コードの全容  
  
ペーストイベントが検出された際に実行される処理を、拡張機能によって制御させるようにします。  
大まかな流れとしては、  

 1. クリップボードに置かれているデータが空の場合（例外処理）
 2.  イベント既定のアクションをキャンセル
 3. 範囲選択した状態でペーストすることを考慮（現在位置から取得したコンテンツを削除）
 4. クリップボードに置かれているデータをTextで③の位置に挿入
 5. カーソルを挿入文字列の末尾に移動
 6. ペーストイベントが検出された際に①〜⑤の処理を実行

のようになるかと思います。  
以下、今回作成したcontent.jsの全容です。ご参考いただければ幸いです。  各コードの挙動については次でご説明いたします。
```javascript
function handler(event) {  
// ①例外処理  
if (event.clipboardData.getData("text/plain") == "") {  
return;  
}  
  
// ②イベント既定のアクションをキャンセル  
event.preventDefault();  
event.stopImmediatePropagation();  
  
// ③現在位置から取得したコンテンツを削除  
let selection = window.getSelection();  
let selectRange = selection.getRangeAt(0);  
selectRange.deleteContents();  
  
// ④クリップボードに置かれているデータをTextで挿入  
let node = document.createTextNode(event.clipboardData.getData("text/plain"));  
selectRange.insertNode(node);  
  
// ⑤カーソルを挿入文字列の末尾に移動  
selection.collapseToEnd();  
return false;  
}  
  
// ⑥ペーストイベントが検出された際に①〜⑤の処理を実行  
window.addEventListener("paste", handler, true);  
```  
  
  
## コードの解説  
### ①例外処理  
```javascript
if (event.clipboardData.getData("text/plain") == "") {  
return;  
}  
```  
`event.clipboardData`プロパティの`.getData()`を呼び出すことで、クリップボードの中身にアクセスし、指定したデータ型を文字列として受け取ります。アクセスしたクリップボードがデータを含まない場合は空文字列を返します。

  

> データ型は、例えばtext/plainやtext/uri-listです。
>引用：[https://developer.mozilla.org/ja/docs/Web/API/DataTransfer/getData](https://developer.mozilla.org/ja/docs/Web/API/DataTransfer/getData)  
>
ここではデータ型をtext/plainとし、アクセスしたクリップボードがデータを含まない場合の処理についてif文で記載しています。  
ちなみに、このif文では`return`以降が省略されているため、戻り値はunderfindとなります。  
  
### ②イベント既定のアクションをキャンセル  
```javascript
event.preventDefault(); // 既定のアクションをキャンセル
 
// 呼び出されているイベントの、まだ実行されていないイベントリスナーをキャンセル 
event.stopImmediatePropagation();  
```  
[こちら]([https://developer.mozilla.org/ja/docs/Web/API/Element/paste_event](https://developer.mozilla.org/ja/docs/Web/API/Element/paste_event))に記載されているように、ペーストイベント既定の動作を上書きしたい場合、イベントハンドラーで`event.preventDefault();`を用いて既定アクションをキャンセルする必要があります。  
  
### ③現在位置から取得したコンテンツを削除  
```javascript
let selection = window.getSelection(); // 現在位置のSelectionオブジェクトを取得  
let selectRange = selection.getRangeAt(0); // インデックスの範囲を選択  
selectRange.deleteContents(); // 取得した範囲を削除  
```  
Selectionは以下のように説明されています。  
> Selectionは、ユーザーが選択した範囲の情報を管理するための機能を備えたインターフェイスです。  
> 引用：[https://lab.syncer.jp/Web/API_Interface/Reference/IDL/Selection/](https://lab.syncer.jp/Web/API_Interface/Reference/IDL/Selection/)

`.getRangeAt()`はSelectionのメソッドのひとつで、指定したインデックスのRangeを取得します。
なお、Rangeは以下のように説明されています。  

> Rangeは、範囲を管理するための機能を備えたインターフェイスです。  
> 引用：[https://lab.syncer.jp/Web/API_Interface/Reference/IDL/Range/](https://lab.syncer.jp/Web/API_Interface/Reference/IDL/Range/)

 
これによってカーソルで指定した範囲を取得することができるため、範囲選択した状態でペーストした際に、選択範囲を上書きする形でペーストする動きが再現できます。  
  
### ④クリップボードに置かれているデータをtext形式で挿入  
```javascript
// HTML文字をエスケープして、()のデータでTextノードを作成  
let node = document.createTextNode(event.clipboardData.getData("text/plain"));  

// selectRangeの開始位置にノードを挿入  
selectRange.insertNode(node);  
```  
`document.createTextNode()`で新しいTextノードを作成します。( )の中身は①と同じものです。  
このメソッドによってHTML文字をエスケープさせることができるため、「ペーストした際に現在位置の先頭に改行が2行挿入される」問題を回避することが可能となります。  
  
### ⑤カーソルを挿入文字列の末尾に移動  
```javascript
selection.collapseToEnd(); // カーソルを挿入文字列の末尾に移動  
return false; // 以降の処理を中断（＝処理終了）  
```  
  
### ⑥ペーストイベントが検出された際に①〜⑤の処理を実行  
```javascript
function handler(event) {  
// ①〜⑤の処理  
}  
window.addEventListener("paste", handler, true);  
```  
`window.addEventListener()`の第三引数でpassiveオプションを`true`にしています。  
このオプションは以下のように説明されています。  

> 論理値で、 true ならば、 listener で指定された関数が preventDefault()
> を呼び出さないことを示します。呼び出されたリスナーが preventDefault()
> を呼び出すと、ユーザーエージェントは何もせず、コンソールに警告を出力します。（以下略）  
> 引用：[https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)

 
  
  
## 余談  
今回ご紹介した拡張機能では、クリップボードに置かれているデータをプレーンテキストの形式で受け取るため、書式を保持したままペーストすることができません。
既に[こちら](#拡張機能で実現したかったこと)で触れていますが、2023/11/27時点のGooge chatのペーストイベントでは、投稿の一部をコピペしても、現在位置から改行が挿入されない仕様となっていました。
こちらは書式を保持したままでペーストしてくれるので「書式を保持しなくない」など特別な意図がなければ、今回作成した拡張機能の出番は無さそうです。

  
## おわりに  
今回は実際に作成した拡張機能の内容について取り上げました。
いつの間にかGoogle chatの方で解消したかった現象が起こらなくなっていたので、作成時よりも必要性の低い拡張機能となってしまいましたが良い経験になりました。

イベントハンドラに対する拡張機能であれば参考サイトも多いので、初心者の方でも比較的作成しやすいかと思います。
興味のある方は、ぜひ挑戦してみてください。
<br>
以上です。閲覧ありがとうございました。
<br>
<br>
出典：
 
- [https://developer.mozilla.org/ja/docs/Web/API/Element/paste_event](https://developer.mozilla.org/ja/docs/Web/API/Element/paste_event)  
- [https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types)  
- [https://developer.mozilla.org/ja/docs/Web/API/Event/stopImmediatePropagation](https://developer.mozilla.org/ja/docs/Web/API/Event/stopImmediatePropagation)  
- [https://developer.mozilla.org/ja/docs/Web/API/Document/createTextNode](https://developer.mozilla.org/ja/docs/Web/API/Document/createTextNode)  
- [https://js.studio-kingdom.com/javascript/range/insert_node](https://js.studio-kingdom.com/javascript/range/insert_node)  
- [https://js.studio-kingdom.com/javascript/selection/collapse_to_end](https://js.studio-kingdom.com/javascript/selection/collapse_to_end)  
- [https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)