この記事は、All About Group(株式会社オールアバウト) Advent Calendar 2023 7日目の記事です。
初めての方ははじめまして、そうでない方はお久しぶりです。
NACKと申します。
今年も「毎月7日は罠(わな=07)の日」……と言い張ってみます。
※記念日認定されているわけではありませんので、ご注意ください。
毎年「罠の日」ということでトラップネタを書いてきましたが、ネタが切れてしまったので、今年は別の話をします。
問題
こんなサンプルページがあったとします。
リストの左に書いてあるのが魚の名前と説明、右に書いてあるのがその魚の旬です。
色々書いてあるのは良いのですが、順番がばらばらで読みにくいです。
利用者としては「指定した季節が旬の魚だけをフィルタしたい」とか「名前順にソートしたい」などと思うわけですが、残念ながら、そのサイトにはフィルタ機能もソート機能も実装されていません。
さてどうしましょうか?
- 諦める
- サイトの管理者に要望を出す
- 自分で頑張る
答え
「諦める」を選んだ場合
正攻法その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ソースが展開されます)
<!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>
このブックマークレットをサンプルページで動かすと、こうなります。
他の条件に変更したい場合
このブックマークレットは汎用的に作っておりますので、設定(各種定数)を修正すれば、他サイトでもうまくいけば動かせると思います。
ただし、 ご利用は自己責任でお願いします。
フィルタ条件を変えたい
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:none
を important
しています。
ソート処理
ソート対象行を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」で動くように改変したものです(といっても、修正箇所は定数部分だけですが)。
本家に並べ替え&フィルタ条件の保存機能が実装されて、このブックマークレットがお役御免になる日が早く来ますように