6
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?

More than 1 year has passed since last update.

All About(株式会社オールアバウト)Advent Calendar 2023

Day 7

「フィルタもソートも無いリスト」にフィルタとソート機能を追加するブックマークレットを作ってみた

Last updated at Posted at 2023-12-06

この記事は、All About Group(株式会社オールアバウト) Advent Calendar 2023 7日目の記事です。

初めての方ははじめまして、そうでない方はお久しぶりです。
NACKと申します。

今年も「毎月7日は罠(わな=07)の日」……と言い張ってみます。
※記念日認定されているわけではありませんので、ご注意ください。

毎年「罠の日」ということでトラップネタを書いてきましたが、ネタが切れてしまったので、今年は別の話をします。

問題

こんなサンプルページがあったとします。

image.png

リストの左に書いてあるのが魚の名前と説明、右に書いてあるのがその魚の旬です。
色々書いてあるのは良いのですが、順番がばらばらで読みにくいです。

利用者としては「指定した季節が旬の魚だけをフィルタしたい」とか「名前順にソートしたい」などと思うわけですが、残念ながら、そのサイトにはフィルタ機能もソート機能も実装されていません。

さてどうしましょうか?

  • 諦める
  • サイトの管理者に要望を出す
  • 自分で頑張る

答え

「諦める」を選んだ場合

正攻法その1です。そのうち慣れると良いですね。

「サイトの管理者に要望を出す」を選んだ場合

正攻法その2です。実装されると良いですね。

「自分で頑張る」を選んだ場合

こういう時は、ブックマークレットを作って、自分でソートやフィルタを実装してしまいましょう(ただし自己責任で)。

というわけで、作ってみた。

そもそもブックマークレットって何?どうやって登録するの?

詳しく説明してくださっている方がいらっしゃったので、こちらをご覧ください。
簡単に言うと「どこもサイト上でも動かせる、自作JSコード」のことです。

作ったブックマークレット

サンプルページから、「夏が旬で、冬が旬ではない魚」を、魚の名前順に取得するブックマークレットです。

ブックマークレット
javascript:( function(){ const filterOkWord = ""; const filterNgWord = ""; const filterSel = ".season"; const sortSel = ".fishname"; const listSel = ".list-group"; const defaultDisplayProp = ""; const sorts=[]; const rows = document.querySelectorAll(listSel + " > *"); rows.forEach(function(row, index){ if (sortSel !== "") { sorts.push({"i":index,"v":row.querySelector(sortSel).innerText}); } if (filterSel !== "") { const filterTxt=row.querySelector(filterSel).innerText; if (filterOkWord !== "" && filterTxt.indexOf(filterOkWord) == -1) { row.style.setProperty("display" ,"none", "important"); return; } if (filterNgWord !== "" && filterTxt.indexOf(filterNgWord) !== -1) { row.style.setProperty("display", "none", "important"); return; } row.style.setProperty("display", defaultDisplayProp, ""); } }); if (sortSel !== "") { sorts.sort((a, b) => { return a.v > b.v ? 1 : -1; }); sorts.forEach(function(sort){ document.querySelector(listSel).appendChild(rows[sort["i"]]); }); } } )();
サンプルページのHTML(長いので折りたたんでいます。クリックするとHTMLソースが展開されます)
サンプルHTML
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ソートもフィルタも無い表のサンプル</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
  </head>
  <body>
    <h1 class="h1">魚の旬</h1>
    <div class="d-flex flex-column flex-md-row p-4 gap-4 py-md-5">
      <div class="list-group">
        <a href="#" class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
          <div class="d-flex gap-2 w-100 justify-content-between">
            <div>
              <h6 class="mb-0 fishname">さわら</h6>
              <p class="mb-0 opacity-75">魚へんに春(鰆)。</p>
            </div>
            <small class="opacity-50 text-nowrap season"></small>
          </div>
        </a>
        <a href="#" class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
          <div class="d-flex gap-2 w-100 justify-content-between">
            <div>
              <h6 class="mb-0 fishname">あゆ</h6>
              <p class="mb-0 opacity-75">魚へんに占(鮎)。</p>
            </div>
            <small class="opacity-50 text-nowrap season"></small>
          </div>
        </a>
        <a href="#" class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
          <div class="d-flex gap-2 w-100 justify-content-between">
            <div>
              <h6 class="mb-0 fishname">すずき</h6>
              <p class="mb-0 opacity-75">魚へんに盧(鱸)。</p>
            </div>
            <small class="opacity-50 text-nowrap season"></small>
          </div>
        </a>
        <a href="#" class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
          <div class="d-flex gap-2 w-100 justify-content-between">
            <div>
              <h6 class="mb-0 fishname">あじ</h6>
              <p class="mb-0 opacity-75">魚へんに参(鯵)。</p>
            </div>
            <small class="opacity-50 text-nowrap season"></small>
          </div>
        </a>
        <a href="#" class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
          <div class="d-flex gap-2 w-100 justify-content-between">
            <div>
              <h6 class="mb-0 fishname">いわし</h6>
              <p class="mb-0 opacity-75">魚へんに弱(鰯)。種類によって旬が違う。</p>
            </div>
            <small class="opacity-50 text-nowrap season">夏、冬</small>
          </div>
        </a>
        <a href="#" class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
          <div class="d-flex gap-2 w-100 justify-content-between">
            <div>
              <h6 class="mb-0 fishname">さんま</h6>
              <p class="mb-0 opacity-75">魚へんに秋……ではなく秋刀魚。なお、魚へんに秋(鰍)は「いなだ、かじか、どじょう」で、いずれも別の魚。</p>
            </div>
            <small class="opacity-50 text-nowrap season"></small>
          </div>
        </a>
        <a href="#" class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
          <div class="d-flex gap-2 w-100 justify-content-between">
            <div>
              <h6 class="mb-0 fishname">ぶり</h6>
              <p class="mb-0 opacity-75">魚へんに師(鰤)。</p>
            </div>
            <small class="opacity-50 text-nowrap season"></small>
          </div>
        </a>
      </div>
    </div>
  </body>
</html>

このブックマークレットをサンプルページで動かすと、こうなります。

image.png

他の条件に変更したい場合

このブックマークレットは汎用的に作っておりますので、設定(各種定数)を修正すれば、他サイトでもうまくいけば動かせると思います。
ただし、 ご利用は自己責任でお願いします。

フィルタ条件を変えたい

const filterOkWord = ""; const filterNgWord = "";

を変更してください。

  • filterOkWord
    フィルタ条件:OK(この文字列が「含まれている」行を表示する。未設定時は判定しない)
  • filterNgWord
    フィルタ条件:NG(この文字列が「含まれている」行を表示しない。未設定時は判定しない)

です(完全一致ではなく部分一致なのでご注意ください)。

変更例:「秋」だけを取得。除外条件は特に無し。
const filterOkWord = ""; const filterNgWord = "";
変更例:取得条件は「全て」、除外条件は「夏、冬」
const filterOkWord = ""; const filterNgWord = "夏、冬";

フィルタ対象項目を変えたい

const filterSel = ".season";

を、フィルタ判定したい文字列が取得できるセレクタ(行要素内で相対的に有効であればOK)に変更してください。
未設定時はフィルタしません。

変更例:魚の名前でフィルタしたい
const filterSel = ".fishname"; // "h6" でも可
変更例:フィルタしない
const filterSel = "";

ソート対象項目を変えたい

const sortSel = ".fishname";

を、ソートしたい文字列が取得できるセレクタ(行要素内で相対的に有効であればOK)に変更してください。
未設定時はソートしません。

変更例:魚の旬でソートしたい
const sortSel = ".season"; // "small" でも可
変更例:ソートしない
const sortSel = "";

ソート対象の表を変えたい

const listSel = ".list-group";

を、ソート対象の表(フィルタ&ソート対象の行の親要素)のセレクタに変更してください。

別のサイトの表でフィルタ&ソートしたい

ここまでの説明に出てきた各項目の値を、そのサイトでフィルタ&ソートしたい表の作りに合わせて、全て修正してください。

「セレクタの変更」について

セレクタを変更するには「その要素がHTML上でどんな構成になっているか(何タグなのか、適用されているid、classは何なのか、など)」と「その構成を、どうセレクタとして設定するか」が必要になります。

下記の記事で詳しく説明されていらっしゃるので、詳細はそちらをご覧ください。

  • 要素の確認方法(下記の記事の「Elementsタブ」項参照のこと)

  • 「確認した要素」を指定するためのセレクタの設定方法

コードの解説

ブックマークレットはワンライナーになっているので、これをフォーマット(他)して、見やすくしてみます。

ブックマークレットのコードを見やすくフォーマット&コメントを追加したもの
javascript:(
    function(){
        // --- ユーザー設定ここから
        // フィルタ条件:OK(この文字列が含まれている行を表示する。未設定時は判定しない)
        const filterOkWord = "";
        // フィルタ条件:NG(この文字列が含まれている行を表示しない。未設定時は判定しない)
        const filterNgWord = "";
        // フィルタオブジェクトのセレクター(フィルタ判定に使用する文字列が取得できる要素を設定。未設定時はフィルタしない)
        const filterSel = ".season";
        // リストオブジェクトのセレクター。これの子要素が「フィルタ&ソート対象」の行になるよう設定する。
        const sortSel = ".fishname";
        // 「フィルタ&ソート対象」オブジェクトのdisplay:○○のデフォルト値。基本は空でOKのはず。
        const listSel = ".list-group";
        // ソートオブジェクトのセレクター(ソート判定に使用する文字列が取得できる要素を設定。未設定時はソートしない)
        const defaultDisplayProp = "";
        // --- ユーザー設定ここまで

        // ソート&フィルタ対象になる行オブジェクト(複数)
        const rows = document.querySelectorAll(listSel + " > *");

        // ソート用の連想配列を入れる配列
        const sorts=[];

        // 1行ずつ回す
        rows.forEach(function(row, index){
            // ソート準備
            if (sortSel !== "") {
                // ソート用の連想配列を入れる配列。i=行オブジェクトのindex、v=ソート対象文字列
                sorts.push({"i":index,"v":row.querySelector(sortSel).innerText});
            }
            
            // フィルター処理
            if (filterSel !== "") {
                const filterTxt=row.querySelector(filterSel).innerText;
            
                if (filterOkWord !== "" && filterTxt.indexOf(filterOkWord) == -1) {
                    // 非表示
                    row.style.setProperty("display" ,"none", "important");
                    return;
                }
                if (filterNgWord !== "" && filterTxt.indexOf(filterNgWord) !== -1) {
                    // 非表示
                    row.style.setProperty("display", "none", "important");
                    return;
                }
                // 表示
                row.style.setProperty("display", defaultDisplayProp, "");
            }
        });
        
        // ソート処理
        if (sortSel !== "") {
            // ソート用の値をソートする
            sorts.sort((a, b) => {
                return a.v > b.v ? 1 : -1;  
            });
            // ソートした値
            sorts.forEach(function(sort, index){
                document.querySelector(listSel).appendChild(rows[sort["i"]]);
            });
        }
    }
)();

ブックマークレットの解説

フィルタ処理

基本的にはコード内にコメントで記載しているとおりです(フィルタ対象文字列が含まれている要素から値を取得して、対象の文字列が含まれるかどうかを判定して、結果に応じてdisplayプロパティを設定しているだけ)。

一点だけ補足すると、表示・非表示の切り替えのところで

row.style.setProperty("display", "none", "important");

という書き方をしています。
こちら、通常は row.style.display = "none"; で十分なのですが、bootstrap等で、CSSでdisplayをimportantで設定されている場合、 row.style.display = "none"; では無効になってしまいます(important指定されているCSSの設定が優先されるため)。
CSSで指定した値を強制的に上書きするため、style設定でdisplay:noneimportant しています。

ソート処理

ソート対象行を1行ずつ回している時に、ソート用に「各行のindexと、ソート用文字列の連想配列」の配列( sorts )を取得します。

全ての行の値を取得すると、sorts の値はこうなります。

[
    {"i": 0, "v": "さわら"},
    {"i": 1, "v": "あゆ"},
    {"i": 2, "v": "すずき"},
    {"i": 3, "v": "あじ"},
    {"i": 4, "v": "いわし"},
    {"i": 5, "v": "さんま"},
    {"i": 6, "v": "ぶり"}
]

これを、v の値でソートすると、こうなります。

[
    {"i": 3, "v": "あじ"},
    {"i": 1, "v": "あゆ"},
    {"i": 4, "v": "いわし"},
    {"i": 0, "v": "さわら"},
    {"i": 5, "v": "さんま"},
    {"i": 2, "v": "すずき"},
    {"i": 6, "v": "ぶり"}
]

並べ替え前の行INDEX値は i に格納されているので、sorts を1行ずつ回して、該当行を、親要素の「子要素」として追加します(このサンプルの場合だと、row[3]、row[1]、row[4]、……の順に追加する)。

// ソートした値
sorts.forEach(function(sort, index){
    document.querySelector(listSel).appendChild(rows[sort["i"]]);
});

なお、appendChildで「追加」しているはずなのに、なぜ項目が増えるのではなく移動するのか……については、下記の記事で解説されています。

余談

本ブックマークレットですが、もともと、社内にて「Power Automateの『マイフロー』のリストを並べ替えたい」という要望があって作ったものです(フィルタ機能は本家にもありますが、「フィルタ条件をブックマークしたい」という要望もあったので、ついでに実装)。
注:この記事に掲載したブックマークレットは「サンプルHTML」で動くように改変したものです(といっても、修正箇所は定数部分だけですが)。

本家に並べ替え&フィルタ条件の保存機能が実装されて、このブックマークレットがお役御免になる日が早く来ますように:pray:

6
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
6
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?