html Javascript プログラミング 未分類

サイト毎に設定したJavascriptを動かすChrome拡張機能を作った

拡張機能やブックマークレットが増えすぎる

Chrome拡張機能やブックマークレットをよく作ります。

サイトの文字を大きくしたり、見たい部分だけ残して他を非表示にしたり、ショートカットを追加したり、動画に部分リピート機能を付けたりしています。

お陰でPCでのWeb閲覧はたいへん快適になりました。

ブックマークレットや拡張機能はPC版ブラウザを使う大きなメリットだと思います。

ただ、ブックマークレットや拡張機能が増えてくると管理が面倒になってきます。

そこで、任意のサイト上で指定したJavascriptを自動実行する拡張機能を作りました。

URLとJavascriptファイルのペアを設定

サイトとJavascriptコードのペアは以下のように拡張機能内のオブジェクトで指定します。

{'URL:''filename.js'}

例:
const urlMappings = {
  "togetter.com": "togetter.js",
  "youtube.com/watch":"youtubeAbRepeat.js",
  "www.amazon.co.jp/s?k=":"amazon.js"
  // 他のURLとスクリプトのマッピングを追加...
};

あとは、background.jsがURLを監視して一致したら対応するJavascriptコードを対象ページに注入してくれます。

拡張機能アイコンの表示と機能

拡張機能アイコンは状態表示とオンオフスイッチを兼ねています。クリックするとON/OFFを切り替えられます。

赤で"ON":拡張機能はオンになっていますが、URLが一致しないのでJavascriptコード注入されていません。

緑で"ON":拡張機能はオンになっており、URLが一致したのでJavascriptコードが注入されました。

赤で"OFF":拡張機能はオフです。URLが一致してもJavascriptコードは注入されません。いったん「緑で"ON"」になった後でOFFにしても注入されたJavascriptは消えません。無効にするためにはページをリロードする必要があります。

表示やON/OFFに関しては、複数タブで同時に使うことは想定していないので、複数タブを開くと表示がおかしい時があります。

コード

コードは以下のようなものです。

URLの判定と対応するJavascriptの注入はbackground.jsが行っています。

単純な部分一致なので、abc.comを設定した場合には、xxxabc.comも注入対象になってしまいます。

manifest.json

{
  "manifest_version": 3,
  "name": "URL-based Script Injector",
  "version": "1.0",
  "permissions": [
    "activeTab",
    "storage",
    "tabs",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_icon": {
      "16": "icons/enabledIcon.png"
    }
  }
}

background.js


// background.js
console.log("background.js loaded");
const urlMappings = {
  "youtube.com/watch":"youtubeAbRepeat.js"
  // 他のURLとスクリプトのマッピングを追加...
};

let isEnabled = true; // デフォルトは有効

chrome.action.onClicked.addListener((tab) => {
  isEnabled = !isEnabled; // 有効/無効を切り替え

  if (isEnabled) {
    chrome.action.setIcon({ path: 'icons/enabledIcon.png' });
    chrome.storage.sync.set({ isEnabled: true });
  } else {
    chrome.action.setIcon({ path: 'icons/disabledIcon.png' });
    chrome.storage.sync.set({ isEnabled: false });
  }
});

// Webページに対するスクリプトの注入をチェックするロジック
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {//タブの更新で発火
  console.log("chrome.tabs.onUpdated");
  if (changeInfo.status === 'complete') {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      const tab = tabs[0];
      console.log(JSON.stringify(tab));
      if (tab.url && !tab.url.startsWith('chrome://')) {
        console.log("check passed");
        chrome.storage.sync.get('isEnabled', (data) => {//chromeのストレージをチェック
          if (data.isEnabled) {
            console.log("switch is Enabled");
            // 有効の場合、urlMappingに沿ってスクリプトを注入する
            for (let [urlPart, scriptFile] of Object.entries(urlMappings)) {
              console.log(tab.url, urlPart, scriptFile)
              if (tab.url.includes(urlPart)) {
                console.log("matched");
                chrome.scripting.executeScript({
                  target: { tabId: tab.id },
                  files: [scriptFile]
                });
                console.log("injected");
                chrome.action.setIcon({ path: 'icons/injectedIcon.png' });

                break;  // URLにマッチした場合はループを抜ける
              }
            }
          }
        });
      }
    });
  }
});

// 拡張機能起動時に前回の状態を取得
chrome.runtime.onStartup.addListener(() => {
  console.log("on Startup")
  chrome.storage.sync.get('isEnabled', (data) => {
    isEnabled = data.isEnabled;
  });
  if (data.isEnabled) {
    chrome.action.setIcon({ path: 'icons/enabledIcon.png' });
  } else {
    chrome.action.setIcon({ path: 'icons/disabledIcon.png' });
  }

});

注入するコードの例として、Youtubeの部分リピートを可能にするコードを挙げておきます。以下の記事で作ったブックマークレットを拡張機能用にしたものです。

Youtubeの動画再生ページで再生中にAボタンを押すとリピート開始時間、Bボタンを押すとリピート終了時間を設定できます。Nを押すとリピートのON/OFFができます。

youtubeAbRepeat.js

(function () {
  const videoElement = document.querySelector("video");
  let repeatTime_A = 0;
  let repeatTime_B = videoElement.duration;
  let enable_loop = false;
  console.log(repeatTime_A, repeatTime_B, enable_loop);

  console.log("A-B repeat bookmarklet is on");

  document.addEventListener("keydown", function (e) {
    if (e.target.tagName.toLowerCase() === 'input') {
      return; // input要素での入力中は何もしない
    }
    const now = videoElement.currentTime;
    console.log(e.key);
    if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return;
    switch (e.code) {
      case 'KeyA':
        repeatTime_A = now;
        if (now > repeatTime_B) repeatTime_B = videoElement.duration;
        enable_loop = true;
        console.log("repeat", repeatTime_A, repeatTime_B);
        break;
      case 'KeyB':
        repeatTime_B = now < 1 ? 1 : now;
        if (now <= repeatTime_A) repeatTime_A = 0;
        enable_loop = true;
        console.log("repeat", repeatTime_A, repeatTime_B);
        break;
      case 'KeyN':
        enable_loop = !enable_loop;
        console.log("repeat", enable_loop);
        break;
    }
  });

  videoElement.addEventListener("seeking", (e) => {
    const now = videoElement.currentTime;
    if (now > repeatTime_B || now < repeatTime_A) {
      enable_loop = false;
      console.log("repeat is off");
    } 
    // else {
    //   enable_loop = true;
    //   console.log("repeat is on");
    // }
  });

  videoElement.addEventListener("timeupdate", (e) => {
    const now = videoElement.currentTime;
    if (enable_loop) {
      if (now > repeatTime_B) {
        videoElement.currentTime = repeatTime_A;
      } else if (now < repeatTime_A) {
        videoElement.currentTime = repeatTime_A;
      }
    }
  });

})();

アイコンファイルも含めたプロジェクト全体をgithubに置いておきます。

https://github.com/ryjkmr/WebScriptInjector

よく使うコード

不要な要素を非表示にする

CSSセレクタで指定した要素をすべて非表示にします。

CSSセレクタはChromeのデベロッパーツールのElementsで消したい要素を右クリックしてCopy Selectorとしたり、条件をChatGPTに伝えて書いてもらうと楽です。

function hideElementsBySelector(selector) {
    var elements = document.querySelectorAll(selector);
    for (var i = 0; i < elements.length; i++) {
        elements[i].style.display = 'none';
    }
}

// 使用例:
// hideElementsBySelector('.your-css-class');

動的なページに対応する

動的に更新されるページは、window.onloadが発火した以降も更新されるため、上記のコードだけでは追加でロードされた要素が消えません。

以下のようにMutationObserverを使ってDOMを監視します。監視するDOMの属性や範囲などを変更する必要がある場合は、configオブジェクトの内容を適宜調整する必要があります。

function hideElementsBySelector(selector) {
    var elements = document.querySelectorAll(selector);
    for (var i = 0; i < elements.length; i++) {
        elements[i].style.display = 'none';
    }
}


// MutationObserverを初期化します
const observer = new MutationObserver(function(mutations) {
    // DOMの変更が発生したときにhideElementsBySelector関数を起動
    hideElementsBySelector('.your-css-class');
});

const config = {
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true
};

// document.bodyを監視対象として指定
observer.observe(document.body, config);

-html, Javascript, プログラミング, 未分類