🏠

DOMDOMタイムス#4: shadow rootやslotタグにも対応できるcontainsがほしい!?

2023/07/03に公開

不定期でDOMやその周辺についてメモを残すDOMDOMタイムス!👶
なんと4週連続更新です。形だけの不定期。

今日は、使ったことがある人も多そうなElement.prototype.contains()についてです。
これはshadow rootやslotタグをスルーしてしまうので、スルーしないcontainsを作ってみましょう🌝

Element.prototype.contains()ってなんだったっけ

Element.prototype.contains()ってなんやねんという方もいるかもしれません(そんな人がこの記事にたどりつくのか!?)ので、まずはその挙動を復習してみましょう。
これは要素同士の包含関係、つまり親子関係を調べるものでしたね。下記のようになります。

<div>
    <p>
        <span>divの子要素</span>
    </p>
</div>
<a>divの子要素ではない</a>
div.contains(span)
=> true
div.contains(a)
=> false

DOM Living Standardによればこんな定義です。

The contains(other) method steps are to return true if other is an inclusive descendant of this; otherwise false (including when other is null).
https://dom.spec.whatwg.org/#dom-node-contains

「おいおい、inclusive descendantって急に言われてもさあ」という感じですが、それもまあ落ち着いて定義をたどればわかります。要するに、要素またぎアリの子要素ってわけです。

An object A is called a descendant of an object B, if either A is a child of B or A is a child of an object C that is a descendant of B.
An inclusive descendant is an object or one of its descendants.
https://dom.spec.whatwg.org/#concept-tree-descendant

descendantの定義はちゃっかり再帰になっていて面白いですね👶

Element.prototype.contains()はshadow rootやslotタグをまたがない

さて、この記事で扱う課題に移ります。

まず、Element.prototype.contains()はshadow rootをまたぎません!
こんな挙動になっています。

<div>
  #shadow-root
    <p>
      <span>div的にはshadowまたぎの子要素</span>
    </p>
</div>
div.contains(span)
=> false

なお、こういうspanのことをdivからみたshadow-including descendantと言います。そのまんまですね🌞

An object A is a shadow-including descendant of an object B, if A is a descendant of B, or A’s root is a shadow root and A’s root’s host is a shadow-including inclusive descendant of B.
https://dom.spec.whatwg.org/#concept-shadow-including-descendant

さてさて加えて、同じShadow Treeの中にいてもslotなんかがあると、やっぱり子要素だとはみなされなくなってしまいます。

#shadow-root
  <div>
    <slot></slot>
  </div>
<span>slotされることを考慮するとこれはdivの子要素と言えなくもない……</span>
div.contains(span)
=> false

もちろん、これらの振る舞いは先ほど紹介したDOM Living Standardの「containsってのは要するにinclusive descendantかどうかを見るのです」という定義に則っているため、ぜんぜん正しい挙動です。
それに、そもそもshadow rootは、関心が異なるサブツリーをまるごと隠蔽する機構だからcontainsがそれをまたげないのは自然ではあります。

でもやっぱり「slotは考慮してくれよ〜」と思う場面もたまにあります。templateタグでめちゃ使われますし。
(こう考えると、shadow rootが「サブツリー隠蔽」のための機構である一方、それがtemplateタグで使われることで、関心が同じツリー同士を混ぜ混ぜする手段にもなっているのが微妙なのかもしれませんね??そんなことないか。ちょっと口が滑りましたネ👶)

なんなら宣言的shadow domとかいうのも出てきちゃったし、今後template増えるんじゃないの!?どうする〜!?
https://developer.chrome.com/en/articles/declarative-shadow-dom/

というわけで、、、

こんな感じならshadow rootもslotも考慮できるのではないでしょうか??

もっといい関数名を募集中です
function containsConsideringShadowAndSlot(parentElement: Element, target: Element | null) {
  if (!target) {
    return false
  }
  if (parentElement.getRootNode() === target.getRootNode()) {
      return parentElement.contains(target)
  }
  let parentOfTarget = target.assignedSlot || target.parentElement || target.parentNode.host || null
  return containsConsideringShadowAndSlot(parentElement, parentOfTarget)
}

要するに、こうなっています。

  1. 子要素の候補として引数に渡される要素からスタートして、slotやshadow dom内にいる可能性を考慮しつつツリーを上にどんどん登っていく
  2. そのうえで……
    a. 親要素として引数に渡された要素と同じroot nodeを持つツリー内にたどりついたら純正containsで判定
    b. 親要素がなければ(=同じツリーにたどりつけなかったならば)falseを返す

2のaが計算量を抑えるためのちょっとした工夫ですね🌛

ちなみにこの実装だとframeはまたげませんが、お好みにあわせてまたぐようにしたら楽しいですね。

感想

whatwgにも提案してみました🌝どうなるのでしょうか??
議論が面白くなったらここでまた紹介してみますね。
「いやそんなんいらんやろ」と言われて終わってしまうのでしょうか!?
https://github.com/whatwg/dom/issues/1214

それでは今日はここまでであります。暑くなってきましたが皆さんお元気で……

GitHubで編集を提案

Discussion