2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

冬だしモダンなWeb言語を習得してみるAdvent Calendar 2024

Day 5

Edgeの拡張機能を自作する(プルダウンの中身を削る)

Posted at

モダンWebにはJavaScriptが必要。
ということでJavaScriptの練習を兼ねてEdgeの拡張機能を作ってみる。

参考

やりたいこと

とあるWebサイトでドロップダウンリストがあり、何十個もの選択肢がある。
自分が使いたいのはそのうち1個か2個だけなので、他のものは表示してほしくない。

image.png

sample.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div class="container">
        <h1>ドロップダウンリスト</h1>
        <select name="dd-list">
          <option value="1">選択肢1</option>
          <option value="2">選択肢2</option>
          <option value="3">選択肢3</option>
    </div>
  </body>
</html>
  • 指定したWebサイトのみで動作
  • 指定した選択肢のみに絞る
  • チェックボックスを欄外に追加し、チェックするとすべてが表示される

こんなことを実現したい

manifest.jsonを作る

拡張機能の大枠を定義するものらしい。動作内容というよりメタデータ的な。

manifest.json
{
    "manifest_version": 3,
    "name": "hoge function",
    "version": "1.0.0",
    "description": "ドロップダウンの選択肢を削るでござるよ",
    "icons": {
        "16": "img/icon16.png",
        "48": "img/icon48.png",
        "128": "img/icon128.png"
    },
    "content_scripts": [
        {
            "matches": [
                "http://127.0.0.1:3000/model03/sample.html",
                "https://fuga.com"
            ],
            "all_frames": true,
            "run_at": document_idle,
            "js": [
                "content.js"
            ]
        }
    ]
}
プロパティ 意味
manifest_version 3 現在は3固定
name hoge function この拡張機能の名前
version 1.0.0 この拡張機能のバージョン
description ※上記参照 この拡張機能の概要
icons 16x16, 48x48, 128x128のアイコン画像 公式的には左記3つの提供が必要らしい
content_scripts/matches URLのリスト この拡張機能が動作する先のURL。ワイルドカード*も使えるらしい
all_frames true <iframe>などで構成されるページのすべてのフレームに対して動作
run_at document_idle 詳しくは参考サイトを参照
js 実行するJavaScriptや適用するCSSの相対パスを記入 同左

なおお試し作成はVSCodeでLive Previewを使っている。
こいつでhtmlファイルのプレビューを表示するとURLが以下のようになるので、どうやら簡易的にWebサーバを立てているっぽい。

image.png

content.js

実際の動作をゴリゴリ書いていく。

まずはベース

content.js
"use strict";

window.addEventListener("load", function() {
});
  • "use strict"
    • コードチェックを厳格に行われるようになる
    • strictモードのコードは高速に実行できる場合がある?
  • `window.addEventListener("load",
    • 動的に生成される要素が表示されてから実行する
    • 今回対象にするページのドロップダウンは恐らく動的に生成されている…んだけど、そういう理解でいい?SPAのようにあとから要素が順に表示されるようなページのことを指してる?

該当のプルダウンを検索する

content.js
window.addEventListener("load", function() {

  // 該当のドロップダウンを検索
  const items = document.querySelectorAll('select[name="dd-list"]');
  // 今回はこれで唯一に特定できるため書かないが
  // querySelectAllで取得した結果複数Hitする場合は
  // 以下でより詳細に検索する
  // for (let i = 0; i < items.length; i++) {
  //   const item = items[i];
  //   ~判定処理
})

属性セレクターの覚書

構文 効果
[attr] attrという名前の属性を持つ要素
[attr=value] attrという名前の属性の値が正確にvalueである要素
[attr~=value] attrという名前の属性の値が空白区切りの語のリストであり、その内の1つが正確にvalueと一致する要素。classとかを対象に使えそう
`[attr =value]`
[attr^=value] attrという名前の属性の値がvalueで始まる要素
[attr$=value] attrという名前の属性の値がvalueで終わる要素
[attr*=value] attrという名前の属性の値にvalueが含まれる要素
[[attr xxxx i] 閉じ括弧の前にiをつけるとvalueを大文字小文字区別せずに比較される
[[attr xxxx s] 閉じ括弧の前にiをつけるとvalueを大文字小文字区別して比較される

該当ドロップダウンの中身を取得する

content.js
window.addEventListener("load", function() {

  // 該当のドロップダウンを検索
  const items = document.querySelectorAll('select[name="dd-list"]');
  // ドロップダウンの中身を取得
  const options = ....
});

ん?ドロップダウンを見つけたはいいが、その中身ってどうやって見るんだろう。
取り敢えずコンソールに出力してみるか。

content.js
window.addEventListener("load", function() {

  // 該当のドロップダウンを検索
  const items = document.querySelectorAll('select[name="dd-list"]');
  // ドロップダウンの中身を表示
  console.log(items);
 });
フォルダ構成
dropdown_ctl\
|`img\
| |`icon16.png
| |`icon48.png
|  `icon128.png
|`manifest.json
 `content.js

iconは適当にペイントで指定サイズ(16x16, 48x48, 128x128)の水色ベタ塗りのpngを用意して設置した。

結果をココにペロッと貼ろうと思ったが想定の何百倍も出力された。
かつ、出力構成がわけわかめ。
image.png
なんだこれ

参考:【技術Tips:2】Webサイトのドロップダウンリスト一括取得/JavaScript

ベタでselectorを引っ張り込んで指定できるらしい。やってみよう。

image.png

Edgeでも同じようにselectorをコピーはできる。コピーした値は以下のとおり

要素のコピー
<option value="1">選択肢1</option>

outerHMTLをコピー
<option value="1">選択肢1</option>

selectorをコピー
body > div > select > option:nth-child(1)

JSのパスをコピー
document.querySelector("body > div > select > option:nth-child(1)")

スタイルのコピー
※できず

XPathのコピー
/html/body/div/select/option[1]

完全なXPathをコピー
/html/body/div/select/option[1]

あ、JSのパスをコピーが一番近いんじゃないかな。

content.js
window.addEventListener("load", function() {

    // ドロップダウンの中身を取得
    const options = document.querySelectorAll("body > div > select > option")
    // 配列型に変換
    const optionsMap = Array.from(options);
    // 内部テキスト(innerText)だけ抽出
    const optionsLists = optionsMap.map(x => x.innerText);
    // 内部テキストを表示
    console.log("options");
    console.log(options);
    console.log("optionsMap");
    console.log(optionsMap);
    console.log("optionsLists");
    console.log(optionsLists);
    console.log("optionsLists.join('\\n')");
    console.log(optionsLists.join('\n'));
});

image.png

おぉ、取れた。
でもこれってselectが複数あった場合どうなるんだろう。

sample.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div class="container">
        <h1>ドロップダウンリスト</h1>
        <select name="dd-list">
          <option value="1">選択肢1</option>
          <option value="2">選択肢2</option>
          <option value="3">選択肢3</option>
        </select>
        <br>
        <select name="dd-list2">
          <option value="1">選択肢4</option>
          <option value="2">選択肢5</option>
          <option value="3">選択肢6</option>
        </select>
    </div>
  </body>
</html>

image.png

image.png

そうだよね。そうなるよね。
selector属性セレクターでもできるのかな。

content.js
window.addEventListener("load", function() {

    // ドロップダウンの中身を取得
    const options = document.querySelectorAll('body > div > select[name="dd-list"] > option');
    // 配列型に変換
    const optionsMap = Array.from(options);
    // 内部テキスト(innerText)だけ抽出
    const optionsLists = optionsMap.map(x => x.innerText);
    // 内部テキストを表示
    console.log("options");
    console.log(options);
    console.log("optionsMap");
    console.log(optionsMap);
    console.log("optionsLists");
    console.log(optionsLists);
    console.log("optionsLists.join('\\n')");
    console.log(optionsLists.join('\n'));
});

image.png

お、できた!これでよさそう?

出力を特定のoptionだけに絞る

実際の画面に近いようにドロップダウンのinnerTextを変更。

sample.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div class="container">
        <h1>ドロップダウンリスト</h1>
        <select name="dd-list">
          <option value="1">徳川 家康</option>
          <option value="2">徳川 秀忠</option>
          <option value="3">徳川 家光</option>
          <option value="4">徳川 家綱</option>
          <option value="5">徳川 綱吉</option>
          <option value="6">北条 時政</option>
          <option value="7">北条 義時</option>
          <option value="8">北条 泰時</option>
          <option value="9">北条 経時</option>
        </select>
        <br>
        <select name="dd-list2">
          <option value="1">選択肢4</option>
          <option value="2">選択肢5</option>
          <option value="3">選択肢6</option>
        </select>
    </div>
  </body>
</html>

これを「北条家」だけに絞る。前方一致で「北条」にする。

と、思いきや先ほどはinnerTextだけに絞り込んでいる。
実際は各optiondisplayfalseにして表示/非表示を制御すると思う。

ので、optionsMapの時点でループ処理しながら1個ずつ表示したい

というか、これって表示であって元の要素を書き換えてなくない?
表示/非表示を制御した新しいoptionsを適用し直す方法も調べないといけない。

参考:Chrome extensionでselectのリストをフィルタする
参考:JavaScriptの文字列マッチングまとめ(indexOf, lastIndexOf)

content.js
window.addEventListener("load", function() {

  // 渡されたoptionのinnerTextが"北条"であるかを判定
  function isHojo(option) {
    return !option.innerText.indexOf("北条");
  }

  // ドロップダウンの中身を取得
  const options = document.querySelectorAll('body > div > select[name="dd-list"] > option');
  // 配列型に変換
  const optionsMap = Array.from(options);
  // 取得した配列でループ処理
  for (var i = 0; i < optionsMap.length; i++) {
    // デバッグ用
    console.log(optionsMap[i].innerText);
    // innerTextが北条であるかを判定
    if(isHojo(optionsMap[i])) {
      // デバッグ用
      console.log("北条が見つかりました");
      // 北条が見つかった場合、表示にする
      optionsMap[i].style.display = '';
    } else {
      // デバッグ用
      console.log("北条以外が見つかりました");
      // 北条以外は非表示にする
      optionsMap[i].style.display = 'none';
    }
  }
});

image.png

image.png

期待通りになったが初期選択されているものが非表示になった「徳川 家康」のままである。一番最初にHitしたものを初期選択にしたい。

content.js
"use strict";

window.addEventListener("load", function() {

  // 渡されたoptionのinnerTextが"北条"であるかを判定
  function isHojo(option) {
    return !option.innerText.indexOf("北条");
  }

  // ドロップダウンの中身を取得
  const options = document.querySelectorAll('body > div > select[name="dd-list"] > option');
  // 配列型に変換
  const optionsMap = Array.from(options);
  // 検索にHitした要素番号を格納するリスト
  let displayOptionIndex = [];
  // 取得した配列でループ処理
  for (var i = 0; i < optionsMap.length; i++) {
    // デバッグ用
    console.log(optionsMap[i].innerText);
    // innerTextが北条であるかを判定
    if(isHojo(optionsMap[i])) {
      // デバッグ用
      console.log("北条が見つかりました");
      // 北条が見つかった場合、表示にする
      optionsMap[i].style.display = '';
      // 検索にHitした要素番号を格納
      displayOptionIndex.push(i);
    } else {
      // デバッグ用
      console.log("北条以外が見つかりました");
      // 北条以外は非表示にする
      optionsMap[i].style.display = 'none';
    }
    // 検索にHitした要素が1件以上であれば、最初の要素を選択状態にする
    if(displayOptionIndex.length > 0) {
      optionsMap[displayOptionIndex[0]].selected = true;
    }
  }
});

image.png

準備は整った!

検索する値をフォームにする

これだとソースにベタで「北条」を入れているので、ここを指定できるようにしたい。

content.js
"use strict";

window.addEventListener("load", function() {

  // 渡されたoptionのinnerTextが"北条"であるかを判定
  function isHojo(option) {
    return !option.innerText.indexOf(filterKeyword);
  }
  // 目的のドロップダウンを見つけた時に、その右にinputを追加する
  let input = document.createElement('input');
  input.type = 'text';
  input.name = 'filterKeyword';
  input.placeholder = 'フィルタするキーワード';
  let select = document.querySelector('body > div > select[name="dd-list"]');
  select.insertAdjacentElement('afterend', input);

  // inputに入力された文字列を取得
  const filterKeyword = document.querySelector('body > div > input[name="filterKeyword"]');
  // ドロップダウンの中身を取得
  const options = document.querySelectorAll('body > div > select[name="dd-list"] > option');
  // 配列型に変換
  const optionsMap = Array.from(options);
  // 検索にHitした要素番号を格納するリスト
  let displayOptionIndex = [];
  // 取得した配列でループ処理
  for (var i = 0; i < optionsMap.length; i++) {
    // デバッグ用
    console.log(optionsMap[i].innerText);
    // innerTextが北条であるかを判定
    if(isHojo(optionsMap[i])) {
      // デバッグ用
      console.log("北条が見つかりました");
      // 北条が見つかった場合、表示にする
      optionsMap[i].style.display = '';
      // 検索にHitした要素番号を格納
      displayOptionIndex.push(i);
    } else {
      // デバッグ用
      console.log("北条以外が見つかりました");
      // 北条以外は非表示にする
      optionsMap[i].style.display = 'none';
    }
    // 検索にHitした要素が1件以上であれば、最初の要素を選択状態にする
    if(displayOptionIndex.length > 0) {
      optionsMap[displayOptionIndex[0]].selected = true;
    }
  }
});

image.png

しかしこれでは初期が空っぽなのでリストがフィルタされていない。

  1. 空っぽの場合はフィルタしないようにする
  2. 指定されたら即座にフィルタする
content.js
"use strict";

window.addEventListener("load", function() {

  // filterKeywordがnullまたは空であるかを判定
  function isEmptyFilterKeyword(filterKeyword) {
    return !filterKeyword || !filterKeyword.value;
  }
  // 渡されたoptionのinnerTextがfilterKeywordで始まる文字列であるかを判定
  function isIndexOfFilterKeyword(option, filterKeyword) {
    return !option.innerText.indexOf(filterKeyword);
  }
  // 目的のドロップダウンを見つけた時に、その右にinputを追加する
  let input = document.createElement('input');
  input.type = 'text';
  input.name = 'filterKeyword';
  input.placeholder = 'フィルタするキーワード';
  let select = document.querySelector('body > div > select[name="dd-list"]');
  select.insertAdjacentElement('afterend', input);

  input.oninput = function() {
    // filterKeywordに入力された文字列を取得
    const filterKeyword = document.querySelector('body > div > input[name="filterKeyword"]');
    // ドロップダウンの中身を取得
    const options = document.querySelectorAll('body > div > select[name="dd-list"] > option');
    // 配列型に変換
    const optionsMap = Array.from(options);
    // 検索にHitした要素番号を格納するリスト
    let displayOptionIndex = [];
    // 取得した配列でループ処理
    for (var i = 0; i < optionsMap.length; i++) {
      if(isEmptyFilterKeyword(filterKeyword) || isIndexOfFilterKeyword(optionsMap[i], filterKeyword.value)) {
        // 表示にする
        optionsMap[i].style.display = '';
        // 検索にHitした要素番号を格納
        displayOptionIndex.push(i);
      } else {
        // 非表示にする
        optionsMap[i].style.display = 'none';
      }
      // 検索にHitした要素が1件以上であれば、最初の要素を選択状態にする
      if(displayOptionIndex.length > 0) {
        optionsMap[displayOptionIndex[0]].selected = true;
      }
    }
  }
});

image.png

image.png

image.png

デフォルトのフィルタキーワードを指定

何でフィルタリングするかは人それぞれなのでベタ指定はできない。
別途defaultFilterKeyworkd.jsを置いて変数をexportし、content.jsimportを試みたがうまく動かせなかったので(原因わからんしエラー内容も貼り付けてなくて申し訳ないが時間がない)、ベタ書きすることにした。拡張機能を追加する人が自分で書き換えてほしい。

content.js
"use strict";
// 初期フィルタキーワードを定義
const defaultFilterKeyword = '北条';

window.addEventListener("load", function() {

  // filterKeywordがnullまたは空であるかを判定
  function isEmptyFilterKeyword(filterKeyword) {
    return !filterKeyword || !filterKeyword.value;
  }
  // 渡されたoptionのinnerTextがfilterKeywordで始まる文字列であるかを判定
  function isIndexOfFilterKeyword(option, filterKeyword) {
    return !option.innerText.indexOf(filterKeyword);
  }
  // 目的のドロップダウンを見つけた時に、その右にinputを追加する
  let input = document.createElement('input');
  input.type = 'text';
  input.name = 'filterKeyword';
  input.placeholder = 'フィルタするキーワード';
  input.value = defaultFilterKeyword;
  let select = document.querySelector('body > div > select[name="dd-list"]');
  select.insertAdjacentElement('afterend', input);
  input.oninput = applyFilterKeyword;

  // 書記ロード時にフィルタを適用する
  applyFilterKeyword();

  function applyFilterKeyword() {
    // filterKeywordに入力された文字列を取得
    const filterKeyword = document.querySelector('body > div > input[name="filterKeyword"]');
    // ドロップダウンの中身を取得
    const options = document.querySelectorAll('body > div > select[name="dd-list"] > option');
    // 配列型に変換
    const optionsMap = Array.from(options);
    // 検索にHitした要素番号を格納するリスト
    let displayOptionIndex = [];
    // 取得した配列でループ処理
    for (var i = 0; i < optionsMap.length; i++) {
      if(isEmptyFilterKeyword(filterKeyword) || isIndexOfFilterKeyword(optionsMap[i], filterKeyword.value)) {
        // 表示にする
        optionsMap[i].style.display = '';
        // 検索にHitした要素番号を格納
        displayOptionIndex.push(i);
      } else {
        // 非表示にする
        optionsMap[i].style.display = 'none';
      }
      // 検索にHitした要素が1件以上であれば、最初の要素を選択状態にする
      if(displayOptionIndex.length > 0) {
        optionsMap[displayOptionIndex[0]].selected = true;
      }
    }
  }
});

image.png

満足。

終えてみて

JavaScriptの勉強になるかと思って触ってみたが、JavaScriptそのものよりもJsonの扱い方などに苦戦した。少なくともループやifなど基本構文は身につけられた気がする。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?