住所から郵便番号を調べるChrome拡張機能を作ってみた（吹き出しを出す方法）

## はじめに

最近引っ越しを考えているのですが、気になるのが引っ越し先の回線速度です。
回線事業者のサイトで契約できるプランを調べることができるのですが、このとき郵便番号の入力が必要になります。
しかし、賃貸情報サイトには住所だけしか書かれておらず、郵便番号が簡単には分かりません。
そこで拡張機能の勉強も兼ねて、住所から郵便番号を調べるChrome拡張機能を作ってみました。

作成した拡張機能はこちらで公開しています。

https://github.com/s-matsumi/postal-code-lookup

## 使い方

1. Chrome上で郵便番号を調べたい住所を選択します
2. 右クリックして「郵便番号を逆引き」を選択します
3. 選択箇所の下部に吹き出しで郵便番号の候補が表示されます
   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331003/ac92bf21-b65e-bfd1-ab82-7bc741a5ee4e.png)
4. 右上のxボタンを押すと吹き出しが閉じます

## 実装

住所から郵便番号を調べる（逆引き）する処理はZIPCODAさんが公開しているWebAPIを使わせていただいています。

https://zipcoda.net/doc

今回私が作成したのは上記のWebAPIを呼び出すためのUIだけです。

### サイト読み込み時の処理

すべてのサイトに対してあらかじめ読み込み時に吹き出しのスタイルと閉じるボタンの処理を挿入します。
これは`manifest.json`で設定できます。

```json:manifest.jsonの一部
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "css": ["main.css"],
      "js": ["main.js"],
      "all_frames": true
    }
  ]
```

`content_scripts`はサイトの読み込み時にCSSやスクリプトを挿入することを設定できます。（詳細な実行タイミングは[公式ドキュメント](https://developer.chrome.com/docs/extensions/reference/api/extensionTypes?hl=ja#type-RunAt)を参照してください。）
`matches`を`<all_urls>`にするとすべてのサイトを対象にすることができます。
今回は`main.css`と`main.js`を挿入します。

#### main.css

`main.css`には吹き出しのスタイルを記載しています。
吹き出しの作り方はサルワカさんを参考にさせていただきました。

https://saruwakakun.com/html-css/reference/speech-bubble

```css:main.cssの一部
#postal-code-lookup-balloon {
  all: initial;
  position: absolute;
  display: inline-block;
  margin-top: 1em;
  padding: 7px 10px;
  width: 300px;
  color: #555;
  font-size: 16px;
  background: #fff;
  border: solid 3px #555;
  box-sizing: border-box;
  z-index: 10000;
}

#postal-code-lookup-balloon:before {
  all: initial;
  content: "";
  position: absolute;
  top: -24px;
  left: 15%;
  margin-left: -15px;
  border: 12px solid transparent;
  border-bottom: 12px solid #fff;
  z-index: 10002;
}

#postal-code-lookup-balloon:after {
  all: initial;
  content: "";
  position: absolute;
  top: -30px;
  left: 15%;
  margin-left: -17px;
  border: 14px solid transparent;
  border-bottom: 14px solid #555;
  z-index: 10001;
}
```

挿入先のサイトで設定されているスタイルの影響を受けないようにするため`all:initial;`でスタイルをリセットしています。

#### main.js

`main.js`には吹き出しを削除する関数を記載しています。吹き出しのxボタンを押したときに呼び出します。

```javascript:main.js
// 吹き出しを閉じる
const postalCodeLookupCloseBalloon = () => {
  const oldballoon = document.getElementById("postal-code-lookup-balloon");
  oldballoon?.remove();
};
```

### 右クリックメニュー選択時の処理

右クリックメニューから「郵便番号を逆引き」を選択すると`background.js`の`chrome.contextMenus.onClicked.addListener`が動きます。

```javascript:background.jsの一部
// 右クリックメニュー選択時に実行
chrome.contextMenus.onClicked.addListener((info, tab) => {
  // どの項目が選択されたのか判定
  if (info.menuItemId === "lookup_postal_code") {
    chrome.scripting.executeScript({
      target: { tabId: tab.id },
      func: () => {
        (async () => {
          // 選択範囲の座標を取得
          const selection = document.getSelection();
          const range = selection.getRangeAt(0);
          const clientRect = range.getBoundingClientRect();

          // すでに吹き出しが存在する場合、吹き出しを削除
          const oldballoon = document.getElementById(
            "postal-code-lookup-balloon"
          );
          oldballoon?.remove();

          // 吹き出しを作成
          const balloon = document.createElement("div");
          // あらかじめ挿入してあるスタイルのIDを指定
          balloon.id = "postal-code-lookup-balloon";
          // 吹き出しの座標を選択範囲の直下に設定
          balloon.style.left = `${window.scrollX + clientRect.x}px`;
          balloon.style.top = `${
            window.scrollY + clientRect.y + clientRect.height
          }px`;

          // 吹き出しの閉じるボタンを作成
          const button = document.createElement("button");
          button.className = "postal-code-lookup-balloon-button";
          button.textContent = "x";
          // あらかじめ挿入してある関数を指定
          button.onclick = postalCodeLookupCloseBalloon;
          balloon.appendChild(button);

          // 結果表示用のdivを作成
          const content = document.createElement("div");
          content.className = "postal-code-lookup-balloon-content";
          balloon.appendChild(content);

          // ZIPCODA APIを実行
          // https://zipcoda.net/doc
          const address = selection.toString();
          const params = new URLSearchParams({ address });
          const res = await fetch(`https://zipcoda.net/api?${params}`, {
            method: "GET",
          });
          if (res.ok) {
            // 結果を吹き出しに追加
            const data = await res.json();
            for (const item of data.items) {
              const zip1 = item.zipcode.substring(0, 3);
              const zip2 = item.zipcode.substring(3);
              const address = item.address;
              const p = document.createElement("p");
              p.className = "postal-code-lookup-balloon-p";
              p.textContent = `${zip1}-${zip2}: ${address}`;
              content.appendChild(p);
            }
          } else {
            // エラーメッセージを吹き出しに追加
            const data = await res.json();
            console.error("postal-code-lookupエラー", data);
            const p = document.createElement("p");
            p.className = "postal-code-lookup-balloon-p";
            p.textContent = data.message;
            content.appendChild(p);
          }

          // 吹き出しをdomに追加
          document.body.appendChild(balloon);
        })();
      },
    });
  }
});
```

## 感想

初めてChromeの拡張機能を作ってみて、Webの知識が少しあれば意外と簡単だということが分かりました。
手軽に便利なものが作れるためフロントエンドの勉強にちょうどいいと思います。