Shadow DOM境界を跨いだ選択範囲の処理を可能にするgetComposedRanges
getComposedRangesは、Shadow DOM境界を跨いだ選択範囲を取得できるSelectionオブジェクトのメソッドです。ShadowRootを指定することで、カプセル化された要素も正確に選択範囲として扱えます。
はじめに
SelectionオブジェクトのgetComposedRangesメソッドがBaseline 2025入りを果たしました。このメソッドは、Shadow DOMを含む文書全体での選択範囲を取得できるようにします。
この記事では、Selectionオブジェクトとそれが持つgetComposedRangesメソッドについてサンプルを交えて解説します。
Selection
Selectionはユーザーが選択したテキストの範囲やキャレットの位置を扱うオブジェクトです。
window.getSelection()を使用して、現在の選択範囲を取得できます。
const selection = window.getSelection();
// 選択範囲の文字列を取得
const text = selection.toString();
Selectionオブジェクトのプロパティの紹介
サンプルテキスト
あさ、眼をさますときの気持は、面白い。かくれんぼのとき、押入れの真っ暗い中に、じっと、しゃがんで隠れていて、突然、でこちゃんに、がらっと襖をあけられ、日の光がどっと来て、でこちゃんに、「見つけた!」と大声で言われて、まぶしさ、それから、へんな間の悪さ、それから、胸がどきどきして、着物のまえを合せたりして、ちょっと、てれくさく、押入れから出て来て、急にむかむか腹立たしく、あの感じ、いや、ちがう、あの感じでもない、なんだか、もっとやりきれない。
選択中のテキスト(selection.toString())
選択要素の開始位置の要素
(selection.anchorNode.textContent, selection.anchorOffset)
選択要素の終了位置の要素
(selection.focusNode.textContent, selection.focusOffset)
選択の種類(selection.type)
上記の例では、Selectionオブジェクトが持つ選択中の文字列の表示と、いくつかのプロパティを紹介しています。
この他にも、Selectionオブジェクトは選択中の範囲の個数を返すrangeCountプロパティや、テキストが選択されているかどうかを表すisCollapsedプロパティなどを持ちます。
テキストの選択範囲やキャレットの変更は、selectionchangeイベントによって検知することができます。
document.addEventListener('selectionchange', () => {
const selection = window.getSelection();
...
});
documentに対して登録しているので、上の例はこのページ内のどこを選択しても動作します。
続いて、Selectionオブジェクトが持つメソッドの紹介です。
Selectionオブジェクトのメソッドの紹介
サンプルテキスト
あさ、眼をさますときの気持は、面白い。かくれんぼのとき、押入れの真っ暗い中に、じっと、しゃがんで隠れていて、突然、でこちゃんに、がらっと襖をあけられ、日の光がどっと来て、でこちゃんに、「見つけた!」と大声で言われて、まぶしさ、それから、へんな間の悪さ、それから、胸がどきどきして、着物のまえを合せたりして、ちょっと、てれくさく、押入れから出て来て、急にむかむか腹立たしく、あの感じ、いや、ちがう、あの感じでもない、なんだか、もっとやりきれない。
選択範囲の追加
(selection.addRange(Range))
選択範囲の削除
(selection.removeAllRanges(), selection.empty())
要素の子を全て選択する
(selection.selectAllChildren(Node))
範囲の変更
(selection.extend(Node, ?offset))
addRangeはRangeオブジェクトを引数に取り、選択範囲に対象を追加します。
FirefoxではRangeオブジェクトを連続して引数に渡すことで、複数の選択をサポートしています。
addRangeを実行する時は、rangeCountプロパティを確認してください。1以上の場合は選択範囲をクリアして0にする必要があります。
先ほどの例では以下のようにrangeCountを確認してからaddRangeを実行しています。
const selection = window.getSelection();
if (selection && textNode) {
// rangeCountの初期化
if (selection.rangeCount > 0) {
selection.removeAllRanges();
}
const range = new Range();
range.setStart(textNode, 54);
range.setEnd(textNode, 63);
selection.addRange(range);
}
例の最後のボタンで紹介したextendは、選択範囲から指定したノードの先頭までを選択範囲に拡張します。
先頭を除く範囲を選択してから押すことで機能を確認できます。
先頭ではなく、任意の位置を指定したい場合は、オフセットを第2引数に渡します。
// 現在の選択箇所からtextNodeの10文字目まで選択範囲を拡張
selection.extend(textNode, 10);
メソッドについても、プロパティと同様に、ここで紹介したもの以外に多数存在します。
しかし、この章の目的はSelectionオブジェクトの概要と基本的な機能を理解するところなので、説明はここまでとします。
getComposedRanges
getComposedRanges()はShadow DOM境界を跨いだ選択範囲の処理を可能にするメソッドです。
const ranges = getComposedRanges({ shadowRoots: [shadowRoot1, shadowRoot2] });
戻り値はStaticRangeオブジェクトの配列です。StaticRangeはRangeと似ていますが、重要な違いがあります。
RangeはDOMの変更に動的に追従するのに対し、StaticRangeは作成時点のDOM状態を固定的に保持します。
オプションにはShadowRootオブジェクトの配列を渡します。
渡したShadowRootオブジェクトに含まれる要素であれば、Shadow DOM内の要素であっても選択範囲として検出されます。
選択範囲にオプションで指定していないShadowRootオブジェクトが持つ要素が含まれる場合は、そのShadow DOMのホスト要素が返されます。
getComposedRangesメソッドの紹介
サンプルテキスト(テキスト全体が閉じたShadow Tree)
あさ、眼をさますときの気持は、面白い。かくれんぼのとき 、押入れの真っ暗い中に、じっと、しゃがんで隠れていて、突然、でこちゃんに、がらっと襖をあけられ、日の光がどっと来て、でこちゃんに、「見つけた!」と大声で言われて、まぶしさ、それから、へんな間の悪さ、それから、胸がどきどきして、着物のまえを合せたりして、ちょっと、てれくさく、押入れから出て来て、急にむかむか腹立たしく、あの感じ、いや、ちがう、あの感じでもない、なんだか、もっとやりきれない。
SafariやIOSのChrome等ではoptionsを含んだgetComposedRangesメソッドが正しく動作しない場合があります。
この例では、サンプルテキスト全体にattachShadow({ mode: 'closed' })しています。
その上で、getRangeAtメソッドと、getComposedRangesメソッドを使った時の比較、およびgetComposedRangesメソッドの引数にサンプルテキストを含むShadowRootを渡した場合と渡さなかった場合の違いを確認できます。
背景色があるテキスト部分を全て含めて選択した場合や、背景色があるテキストを一部含めた場合、サンプルテキスト以外も含めた選択を試してください。
getComposedRangesメソッドにオプションを渡した場合はstartContainerとendContainerに正確な要素が渡りますが、それ以外はShadow Treeを含む要素全体が返されます(MacOSのChromeを用いた結果のため、他のブラウザでは異なる結果になるかもしれません)。
このように、適切なオプションを付与したgetComposedRangesメソッドを用いることで、Shadow DOM境界を跨いだ選択が行われたとしても、他の要素と同じようにShadow DOM内の要素を正しく扱えるようになります。
おわりに
Selectionオブジェクトについての簡単な紹介と、Shadow DOM境界を跨いだ選択範囲の処理を可能にするgetComposedRangesメソッドについて解説しました。
getComposedRangesを使用することで、通常のDOMとShadow DOMを統一的に扱い、より柔軟なテキスト選択機能を実装できるようになります。