CSS で inner-text を条件にしたセレクタを書いてみたいなぁ
CSS の属性セレクタを使うと、「~~の文言を含む要素」みたいなのを指定できる。
p[data-example*="ほげ"] {
color: #f00;
}
<p data-example="ほげほげ">コレは赤文字になる</p>
<p data-example="ふがふが">コレには適用されない</p>
属性セレクタの色々な書き方は以下のデモを参照。*=
の他に ~=
も覚えておくと便利かも。
- デモ : Practice CSS Attribute Selector
- コード : frontend-sandboxes/index.html at master · Neos21/frontend-sandboxes
また、:empty
擬似クラスを指定すると、一切の子ノードを持たない要素の指定もできる。:empty
って内側の改行や空白文字も存在するとダメみたいなのでシビア。
p:empty {
border: 10px solid #f00;
}
<!-- ↓ コレは赤い枠線が付く -->
<p></p>
<!-- ↓ コレは子要素があるので NG -->
<p><span></span></p>
<!-- ↓ コレは改行があるので NG -->
<p>
</p>
2022年7月時点ではまだ対応しているブラウザがほぼないけど、:has()
が使えるようになると、「こういう子要素を持つ親要素」を指定できる。
.parent:has(.child) {
color: #f00;
}
<div class="parent">
<div class="child">この .parent は赤文字になる</div>
</div>
<div class="parent">
この .parent は対象外
</div>
…さて、ココまでやってきて、innerText
を条件にした CSS セレクタを書いてみたいなぁ、と思った次第。想像上のセレクタだけどこんなことがしたい。
/* 実際には存在しないけど、こんなセレクタを書けたらいいなー */
div:inner-text*="ほげ" {
color: #f00;
}
<div>この div 要素内には「ほげ」の文字が出てくるので赤文字になる</div>
<div>この div 要素は対象外</div>
:inner-text
でも :text-content
でも良いんだけど、こういうの出来ないかね?
ブラウザの Stylus 拡張機能で、ブラウザごとにユーザスタイルシートを書く時なんかにこういうことができるとメッチャ便利なんだよなー。Greasemonkey みたいなユーザスクリプトを書くのって大変だし、DOM 監視とかを自分でやらないと DOM 増減にも対応できないよね。だから CSS セレクタで記述できてブラウザネイティブに解釈してくれたらいいのになーなんて思ってた次第。
調べてみると、前述の :has()
については以下に Polyfill が存在する。
この中の browser-global.js
で、:has()
ではなく独自属性を HTML 側に書き込んで、属性セレクタでスタイルを当てる、そして DOM 要素の変更などを MutationObserver で監視してその都度スタイルが当たるように独自属性の書き込みをやり直している、という仕組みだった。以下で勉強のため動作確認している。
- デモ : Practice CSS Has Pseudo
- コード : frontend-sandboxes/index.html at master · Neos21/frontend-sandboxes
頑張れば JS でこうした Polyfill を作れると分かったのだが、何やかんや大変そうなので、まずは簡単にそれっぽいモノを実現できないかと思ってコードを書いてみた。
つーワケで、とりあえず動くようになったサンプル実装は以下。
- デモ : PoC InnerText Selector
- コード : frontend-sandboxes/index.html at master · Neos21/frontend-sandboxes … 実装はココを見てみてください
現状は一旦、JS 側で全ての処理をするようにしている。こんな感じで呼び出す。
// div:inner-text*="ほげ" { color: #f00; }
// ↑ こういう風に書きたかったのを、↓ のように JS で実現した
cssInnerText('div', '*=', 'ほげ', 'color: #f00;');
第1引数は CSS セレクタを書く。document.querySelectorAll()
にそのまま渡すのでそれに対応している記法なら使える。
第2引数部分は部分一致とか完全一致とかを指定できる。属性セレクタと同じ =
、^=
、$=
、*=
が使える他、=i
とか *= I
とかいう風に書くとケースインセンシティブ (大文字・小文字を区別しない) になる。さらに :not(^=)
というような Not 指定もできる。
第3引数はマッチさせたい innerText
のテキストを指定する。
第4引数に指定したい CSS を書く。現状は Element.style.cssText
に結合してスタイル適用しているので、color: #f00; border: 1px solid #00f; font-weight: bold;
なんつって複数の CSS を連ねた文字列を渡してやっても良い。
他に、第2・第3引数のところを正規表現オブジェクトでも指定できるようにもしてある。
// 第2・第3引数部分を正規表現オブジェクトにしても動くようにした
cssInnerText('div > p', /ほげ/i, 'color: #f00');
やっていることは単純で、本質的なコードは以下に収束する。
document.querySelectorAll('CSS セレクタ').forEach((element) => {
if(new RegExp('マッチさせたい言葉').test(element.innerText.trim())) {
element.style.cssText += 'color: #f00; だとか付与したい CSS';
}
});
なので、前述の css-has-pseudo
Polyfill と比べると、:hover
だとか ::before
だとかそういった擬似クラス、擬似要素をスタイリングできないのが難点。
また MutationObserver なども用意していないので、DOM 要素が後から追加されたりすると上手くスタイル適用されない。再度 cssInnerText()
を呼んでやらないといけない。
本当は、JS ライブラリを読み込みさえすれば div:inner-text=""
なんて書いた CSS 部分を自動的に解釈して処理してくれるようにしたいのだが、そうすると実装が大変だ。
- ページ内で読み込まれた CSS を全て拾い上げ、
:inner-text*=
などと書かれたセレクタ部分を上手くパースする- 当然その前後で
:hover
だの::before
だの指定されていたらそれも後で処理できるようにパースする必要がある - jQuery で使われる Sizzle みたいな CSS パーサを作らないといけない感じ
link
要素やstyle
要素を動的に追加した場合はそれも解釈するように監視しないといけない
- 当然その前後で
- パースしたセレクタに応じて DOM 要素を探索し、目印となる
data
属性を付与していく- 複数の CSS ルールに合致する要素にはどんな風に目印を付けるべきか?ルールごとに ID 値を生成してそれを指定するとか?
<div data-inner-text="[ div:inner-text^='ほげ', form > div:inner-text*='ほげ'::before ]">
こんな風にdata
属性値の中で配列っぽく複数指定できるようにするとか?- それにしてもやっぱり大変…
- そしてコレも、DOM 要素の増減に応じて
data
属性の付与処理をやり直す必要がある
- パースした CSS ルールを
data
属性セレクタに変換してstyle
要素を組み立てる- 最終的に何らかの方法で属性セレクタでの指定に落とし込めれば、擬似要素や擬似クラスにも対応できそうだけど、それが大変なのよね
つーワケで色々面倒臭くなってしまったのでココまで…。誰かやる気のある人、続き実装してください…。もしくは CSS の新しい記法として :inner-text
を作ってください…。