zaki work log

作業ログやら生活ログやらなんやら

[Chrome拡張作成] 特定のサイトのみでコンテキストメニューを表示する

特定のサイトでのみ動作するChrome拡張を作る場合、セオリーというとmanifest.jsoncontent_scripts内でmatchesを使って指定できるが、Chrome拡張でコンテキストメニュー(右クリックメニュー)を作成するためにはbackgroundから指定するサービスワーカーで実装する。
ただしbackgroundを使う場合は、調べた限りではサイト指定の仕組みが現状ないため、サイトごとのon/offを自前で実装する必要がある。たぶん。

この記事ではサイト(URL)ごとにon/offを切り替える具体的な実装についてのメモ。

コンテキストメニューの実装

コンテキストメニューを作成するにはchrome.contextMenusを使用する。

developer.chrome.com

インストール時 (chrome.runtime.onInstalled)

タブやウインドウ切替の度に作成・削除を行うのでここは無くても良いかもしれないが、基本部分なのでとりあえず。
サイトごとの制御が不要であれば、ここだけで良い。

chrome.runtime.onInstalled.addListener(function(details) {
    chrome.contextMenus.create({
        id: "item1",
        title: 'hello extension',
        contexts: ["all"]
    });
});

developer.chrome.com

ページ遷移時 (chrome.tabs.onUpdated)

同一のタブ内でサイトAからサイトBへ遷移したような場合のイベントリスナーを設定できる。
これを使うと「処理対象にしたいサイトに遷移した場合にメニューを有効化」と「対象外に遷移した場合にメニューを無効化」する。
開いているページのURLはコールバック関数の第3引数にタブ情報があるのでそこから参照する。

chrome.tabs.onUpdated.addListener(function(id,info,tab){
    if(tab.active){
        if (tab.url.match(target)) {
            chrome.contextMenus.create(createProperties);
        }
        else {
            chrome.contextMenus.remove("item1");
        }
    }
});

developer.chrome.com

タブ切替時 (chrome.tabs.onActivated)

既に開いているタブA・タブB・…を切り替えた場合のイベントリスナーを設定する。
これで「処理対象にしたいサイトを開いてるタブをアクティブにした場合にメニューを有効化」と「対象外のサイトを開いてるタブをアクティブにした場合にメニューを無効化」する。
開いているページのURLはコールバック関数の引数を元に、タブの情報を取得するchrome.tabs.get()を使用して取り出す。

chrome.tabs.onActivated.addListener(function(info){
    chrome.tabs.get(info.tabId,function(tab){
        if (tab.url.match(target)) {
            chrome.contextMenus.create(createProperties);
        }
        else {
            chrome.contextMenus.remove("item1");
        }
    });
});

注意点として、同一ウインドウ内のタブ切替に有効。別ウインドウへの切替時はこのリスナーは反応しない。

developer.chrome.com

ウインドウ切替 (chrome.windows.onFocusChanged)

前述のタブ切替でなく、ウインドウを切り替えたときのイベントリスナーを設定する。
これで「処理対象にしたいサイトを開いているウインドウがアクティブになった場合にメニューを有効化」と「対象外のサイトを開いているウインドウがアクティブになった場合にメニューを無効化」する。
リスナーに登録する関数内でタブ情報をchrome.tabs.queryで取得する。

chrome.windows.onFocusChanged.addListener(function(info){
    // ウインドウ切替によるアクティブページの変化
    chrome.tabs.query({'active': true, 'currentWindow': true}, tabs => {
        if (tabs[0].url.match(target)) {
            chrome.contextMenus.create(createProperties);
        }
        else {
            chrome.contextMenus.remove("item1");
        }
    });
});

developer.chrome.com

作成・削除の多重処理対策

上記のコードのままだと、タブやウインドウ切替時に多重処理が発生するとエラーになる。

遷移 エラー
処理対象のサイト -> 処理対象のサイト Unchecked runtime.lastError: Cannot create item with duplicate id <ID名>
対象外のサイト -> 対象外のサイト Unchecked runtime.lastError: Cannot find menu item with id <ID名>

APIリファレンスを見た限り「既にcreate済み/remove済みだったら何もしない」は難しそうだったため、エラーを無視する実装にした。
エラーを無視するには以下の通り。

if (tab.url.match(target)) {
    chrome.contextMenus.create(createProperties, () => chrome.runtime.lastError);
}
else {
    chrome.contextMenus.remove("item1", () => chrome.runtime.lastError);
}

stackoverflow.com

実装例

コード全体は以下。

github.com

この機能を使って作成したChrome拡張は、「Azure Portalを開いているときに、右クリックメニューでリソースIDをクリップボーをへコピーする」というもので、以下で公開中。

chrome.google.com

(余談) 機能的にはツールバーの拡張アイコン押下で実装できそうだった(これだとcontent_scriptsのURL指定で処理対象サイトを制限できる)が、「取得した情報をクリップボードに書き込む」が(Documentにフォーカスしていないと動作しない仕様に対する処理が)難しくて断念。

blog.hog.as

Appendix

create/removeでなくupdateは?

コンテキストメニューの処理にはupdateメソッドもあり、パラメタにenabledvisibleがあるので、これで切り替えもできる。
と思うんだけど、指定の方法がおかしいのかupdateのキーになるidをうまくセットできなくて未検証。

Deprecatedになったリスナー登録

Manifest ver2のときにあった以下のイベントリスナーはver3で無くなった。

拡張アイコンでポップアップを特定サイトでのみ表示する

サンプルコードが"Emulating pageActions with declarativeContent"に載っている。

developer.chrome.com

参考