複数のウィンドウを同時にスクロールさせるChrome拡張作った話

本記事は[SLP KBIT AdventCalendar 2022](https://adventar.org/calendars/7569)の6日目の記事になります．
大突貫工事ですが，緩くお楽しみください．

## DEMO
とりあえず開発物のDEMO動画です．
タイトルのとおり，複数のウィンドウを同時に自動スクロールさせることができます(1つでもOK)．
画面右上のポップアップから対象としたいページを選択して，中央のスタートボタンを押すとスクロールが開始します．
右のボタンでスピードアップ，左のボタンでスピードダウンです．
これらのボタンを以降ではスクロール制御ボタンと呼びます．
![ezgif-2-af32aa2a12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309137/3ed6ab72-c014-0833-2e97-fed343f19ffa.gif)

## 開発に使ったもの
- [Chrome Extension API](https://developer.chrome.com/docs/extensions/reference/)
- JavaScript/HTML/CSS

## 開発のお話
今回この記事では，システムの中心的な機能についての話に話題を絞ります．
Chrome拡張の開発のいろはについては，このあたりの記事を参照してみてください．
- [Chrome Extension の作り方 (その1: 3つの世界)](https://qiita.com/sakaimo/items/416f36db1aa982d8d00c)
- [Chrome拡張機能 manifest.json Ver.3の書き方](https://qiita.com/shiro1212/items/12f0a767494a7b2ab0b3)

### システム概要
このシステムは大まかにはbrowser action(ポップアップ)とContent Scriptsの2つのプログラムから構成され，必要な処理は次のとおりです．
1. ポップアップメニュー起動時に，アクティブなタブを取得
1. スクロール制御ボタン押下時に，選択されたタブに対してスクロール制御命令を送信
1. スクロールの実行

#### アクティブなタブの取得
chrome.tabs APIの`chrome.tabs.query`で一撃です．
波括弧の中で取得したいタブの条件を指定し，コールバックに対して任意の処理を追加します．
今回はアクティブ(各ウィンドウで表示されている状態)なタブを取得したいため，`active: true`を条件としています．
また，取得したタブはユーザから選択できるようにするために，チェックリストとして表示するようにしています．
チェックリストにはタブのIdをvalueとして入れています．後で使います．
※このAPIを利用するためには，manifestのpermissionにtabsを追加しておく必要があります．
※このコードはポップアップ(browser action)内に書いています．

```JavaScript
chrome.tabs.query({'active': true}, tabs => {
    // 任意の処理
    for (tab of tabs) {
        content += `<div><input type='checkbox' id='${tab.windowId}' name='activeWindow' value='${tab.id}' /><label for='${tab.windowId}'>${tab.title}</label></div>`
    }
    div.innerHTML = content;
});
```

#### スクロール制御命令の送信
実際に画面のスクロールを担当するのは，Content ScriptsなのでポップアップからContent Scriptsに向けて命令を送信することになります．
そして，この命令の送信も`chrome.tabs API`でできます．今回の開発ほぼtabsしか触ってないですね()
命令を送信したい対象のtabIdとJSON形式のデータをセットにしてメッセージを送信します．
データ取得の命令など，返却値が欲しい場合は先述のようにコールバックに対して処理を実装することもできます．
今回はユーザが選択したすべてのタブに対してスクロール制御命令を送ることで，複数ウィンドウの制御をしています．
スクロールの制御自体は後述しますが，Content Scriptsに任せるので，コールバックはなしです．

```JavaScript
document.getElementById("start").onclick = function() {
    // 選択されたチェックボックスを取得
    let selectOption = document.querySelectorAll("input[name=activeWindow]:checked");
    for (option of selectOption) {
        // ここで命令送信
        chrome.tabs.sendMessage(Number(option.value), {method: "startScroll"})
    }
}
```

#### スクロールの実行
今回の記事のラストです．
ポップアップから送信されたメッセージの受信と，スクロール制御の2つからなります．
まずメッセージの受信です．
表記のとおり，`chrome.runtime.onMessage.addListener`によって受信待ちをしているイメージです．
この中で，送信されたメッセージに対して実現したい任意の処理を書いてやります．
今回はmethodの種類で場合分けをしたのでswitch文での制御になっていますが，やり方は色々あると思います．
```JavaScript
chrome.runtime.onMessage.addListener(function(request) {
    // 任意の処理
    switch (request.method) {
        case 'startScroll':
            if (state == 'scroll') {
                state = 'stop';
            } else {
                y = y >= document.documentElement.scrollHeight ? 0 : y;
                pace = 1;
                state = 'scroll';
            }
            scrollPage(5);
            break;
        ･･･
        case default:
            console.log('no method');
    }
})
```

最後にスクロール制御です．
ざっくりとは，`window.scrollTo()`で移動先を指定してスクロールを実行して，setTimeoutによってそれを一定間隔で繰り返し実行するだけです．
ただ，それだけではいつまで経ってもスクロールし続けてしまうため，以下の場合に停止するように条件を追加しています．
- スクロール可能領域を超過(yの条件)
- ユーザによる停止命令の発火(stateの条件)

現状ではあくまでもウィンドウのスクロールができるだけなので，ウィンドウ中の任意のスクロール領域を動かすことはできません．

```JavaScript
function scrollPage(speed) {
    y += pace;
    window.scrollTo(0, y);
    if (y < document.documentElement.scrollHeight && state == 'scroll') {
        setTimeout(() => {
            scrollPage(speed);
        }, speed);
    } else {
        state = 'stop';
    }
}
```

## おわりに
今回開発したものはGitHubにて[multiScroller](https://github.com/gacky35/multiScroller)という名前で公開しています．
実はmanifestをv2で書いていたり，スクロールの終了がアイコンでわからなかったりと杜撰なとこがかなりありますが興味があれば見てみてください(それ以上にこの記事が…という説はある)．
かなり用途が不明なので，何かしら使い道を見出してもらえたら喜びます．
複数人から別ファイルでもらったレビュー箇所の照らし合わせくらいはできますかね．

それでは，明日以降の記事もお楽しみください．