Chrome拡張でサイトのHTMLを書き換える

# 概要

+ Google Formsで自分の回答の字数を知りたい！
+ Chrome拡張ってどうできているの？何ができるの？

という思いから<b>Google Formsの自分の回答に字数を表示する拡張機能</b>を作りました。

外部(Chrome Web Store)に公開するとなるとお金がかかったり別な手続きが必要みたいですが、自分のChromeで使うだけの拡張であれば、フォルダーをアップロードするだけです。

# Chrome拡張の作り方

今回くらいシンプルな拡張であれば、

* 必要事項を書いた``manifest.json``ファイル
* プログラムを書いた``content-script.js``ファイル（名前は何でもよい)

以上の2つの入ったフォルダをGoogle Chromeにアップロードするだけです。基本的にJavascriptで書きます。


# manifest.jsonファイルについて

Chromeの拡張機能を作るうえで必須なのが`manifest.json`ファイルです。アップロードするフォルダー(ディレクトリ)の直下に作ります。今回は大したことをしないので書くことはあまりありません。



```manifest.json
{
  "manifest_version": 3,
  "name": "Word Counter for Google Forms",
  "version": "1.0.0",
  "description": "This extension shows the number of letters you are writing on a Google Form.",
  "content_scripts": [{
    "matches": ["https://docs.google.com/forms/*"],
    "run_at": "document_end",
    "all_frames": true,
    "js": ["content-script.js"]
  }]
}
```

* ``manifest_version``によってChrome拡張の仕様が少し異なるので、ググるときは気を付けましょう。
* `["content-script.js"]`の箇所は、プログラムを書いたファイル名と一致させてください。

URLが`https://docs.google.com/forms/*`にマッチするWebサイトを開くと`content-script.js`ファイルが実行されますよ～、というだけの内容です。`*`はワイルドカードの一つで、任意の文字列にヒットします。

# コードを書く前に

<details><summary><b>Formの構造を調べる</b></summary>

### この拡張を作成するうえで一番面倒な作業があります

HTMLを書き換えるためにはあらかじめページの構造がわかっていないといけません。
というわけで調べます。

字数を表示したい対象のFormをChromeで開きます。自分で適当なFormを作ってもいいです。

`f12`か`Ctrl`+`Shift`+`I` (または`F12`)で開発者ツールを開き、左上のカーソルマークを押してからフォームの取得したい要素をを見つけていきます。

一つの要素に複数のclassが割り当てられていることもあるので、同じclass名をもつ要素を比較したりして、Googleがどんなページの作り方をしているのか探していかなければいけません。

めっちゃメンドイ...
![スクリーンショット (68).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637844/9735b315-918d-473f-d131-7ed681a4335e.png)
結果として、テキストの回答枠(`input`要素と`textarea`要素)のclass名はすべて`hhh`で、質問文のclass名はすべて`G4EHhc`で、それぞれの親要素のclass名はすべて`geS5n`だとわかりました。

ちなみにGoogleがページの構造変えたら使えなくなる可能性大です(苦笑)

</details>


上記をまとめると、フォームは大まかにこういう構造になっていることが分かります。

```html: サイトのHTMLを超簡略化したイメージ
<div class='geS5n'>
  <span class='G4EHhc'>質問文</span>
  <textarea class='KHxj8b'>複数行の回答内容</textarea>
</div>

<div class='geS5n'>
  <span class='G4EHhc'>質問文</span>
  <input class='whsOnd' value='一行の回答内容'>
</div>
```
回答内容の文字数を取得して、その上の質問文の末尾に`<span class='word-count'>8文字</span>`みたいな要素を付け足します。

初めページを読み込んだ時にその要素を付け足したら、あとは入力があるたびに文字数を上書きしていく、というのが大まかな流れです。

# メインのコード

```JavaScript:content-script.js
'use strict';

const countWords = () => {
  // 入力欄をすべて取得
  const textareas = document.querySelectorAll('.geS5n textarea.KHxj8b, .geS5n input.whsOnd');
  
  // 文字数表示枠をすべて取得
  let counts = document.querySelectorAll('.geS5n .word-counter');

  if (counts[0]) {
  // 文字数表示枠がすでに作られているとき
    for (let i = 0; i < textareas.length; i++) {
      counts[i].innerHTML = ` ${textareas[i].value.replace(/\s/g, '').length}文字`;
    }

  } else {
    // 文字数表示枠がまだ作られていないとき
    for (const textarea of textareas) {

      // 回答欄の親要素の子要素(兄弟要素？)である質問文の要素を取得
      const countDisplay = textarea.closest('.geS5n').querySelector('.G4EHhc');
      
      // 文字数表示枠の要素を作る
      const count = document.createElement('span');
      count.className = 'word-counter';
      count.style.color = '#00f';
      count.innerHTML = ` ${textarea.value.replace(/\s/g, '').length}文字`;

      // 作った要素をcountDisplayの子要素として追加
      countDisplay.appendChild(count);
    }
  }
};

// ページ読み込み後に実行するよう設定
window.addEventListener('load', () => countWords(), false);
// 入力があったら実行するよう設定
window.addEventListener('input', () => countWords());

```
ここで出てくる意味不明な文字列`geS5n`や`G4EHhc`は取得したい要素のclass属性の値です。この後詳しく説明します。

### コードの補足

<details><summary>基本的な文法のはなし</summary>

```javascript
'use strict'; //厳格モードを使用。意図しないエラーやミスを減らすため。

const func = () => {
}; // アロー関数
const func = fundtion() {
}; // ほぼ同じ(正確にはちょっと違う)
const func = () => 'Hello World'; // 一行で書ける(この時returnがいらない)

// バッククォートでくくると文字が扱いやすい。`${この中はjavascript}ここはテキスト`
// テンプレートリテラルとかいったか
const t = 'もじれつ';
const string = `これは${t}です。` // >> これはもじれつです。
const text = 'これは' + t + 'です。' // 同じ
```
</details>

```Javascript:
document.querySelectorAll('.geS5n .word-counter');
````
クラス名が`geS5n`の要素の子要素で`word-counter`クラスを持つ要素をすべて取得する。カンマで区切ればいづれかにヒットするものすべてを取得します。

`document.qruerySelector()`なら初めにヒットした一つを取得。

参考：[要素の取得方法まとめ - Qiita](https://qiita.com/amamamaou/items/25e8b4e1b41c8d3211f4)



```JavaScript:
counts[i].innerHTML = ` ${textareas[i].value.replace(/\s/g, '').length}文字`;
```
`.length`で文字数がわかるのですが、改行や空白なども含まれているので先に`.replace()`で空白文字を消します。

```javascript
if (counts[0]) {}
```
`counts`自体はnullにならないので`counts[0]`で一つ目の有無を見るか、`counts.length`で存在確認する必要があります。つまづきポイント。

```javascript
window.addEventListener('input', () => countWords());
```
`"change"`のイベントリスナーもあるのですが、こちらは入力内容を確定しないと動きません。リアルタイムで変更したいので`input`を使います。

# アップロード
Chrome右上のボタンからManage Extensionをクリックすると、拡張を管理する画面を開けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637844/9096bfa1-8c97-a4df-7270-2525c0c9c00e.png)
ページ右上のDeveloper modeをオンにし、左上のLoad Unpackedから、今回作成したプログラムの親フォルダーを開けば完了です。プログラムを書き換えた場合でも、再読み込みすればよく、アップロードしなおす必要はありません。


# 結果
いい感じ。
![スクリーンショット (66).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637844/ff38f288-ac12-1ee6-b65e-b18b5ed4038e.png)

ただ、サイトのJavaScriptの実行完了のタイミングと`countWord()`の実行開始のタイミングがシビアだからか、読み込み直後、0文字でなくても表示が0文字になってしまうことが時々あります。`setInterval`で監視すればいいのかな？~~めんどくさいので~~不便を感じないのでこのままでいきます。

以上、超基本的なChrome拡張でした。
Googleのサイトの構造は複雑なので気を付けましょう。