DOM Rangeについて色々

誤解していたこと

最終更新
2005-03-23T14:00:55+09:00

これまで、Rangeインターフェイスはテキストの選択範囲を操作する、それだけの仕様だと思っていました。しかし、DOM CoreやHTMLでは面倒で、コードが煩雑になりがちだった複数の要素に関する操作が、Rangeインターフェイスを利用することで直感的かつ直接的な操作になり、とても扱いやすくなることが分かりました。現在では個人的に、DOMで文書ツリーを扱う時には常に傍らにいてもらいたい介さんのような存在になっています。取りあえず呼んでおけ、みたいな。

以下、とてもありがちな要素に関する操作を、CoreやHTMLのみを用いた方法とRangeを利用した方法で行い、その比較を行います。

タスク1 DIV要素をカラッポにする

最終更新
2005-03-23T14:00:55+09:00

DIV要素を何らかの表示用コンテナとして使い、イベントが発生した時に中身をそっくり変更するというケースはよくあると思います。次のようなDIV要素を考え、変数divで参照できる状態であるとします。

<div>
<h1 />
<p />
<h2 />
<p />
</div>

さて、改行などを含めると、DIV要素の子要素は9もあります。これらを一つずつ消去していくのがCoreのスタイルです。

Coreな方法
while(div.lastChild){
  div.removeChild(div.lastChild);
}

あれ? 見た目的には悪くないかシンプルだし。でもあまり直感的ではないと思います。「最後の子がnullでない限り、最後の子を取り除き続けろ」という命令ですから。それに多くの人はfor文を使うでしょうから、コードの読みづらさは一般的にこんなものじゃあないと思います。

また、どちらにしろ子要素の数だけ繰り返しremoveChildを行うのであり、子要素の数によっては相当に効率が悪くなるはずです。

Rangeな方法
var range = document.createRange();
range.selectNodeContents(div);
range.deleteContents();

ステートメントの数こそ多くなってしまいますが、どうです? 直感的だと思いませんか? 「div要素の内容を選択して、それを削除しろ」という、そのまんまの命令です。

タスク2 複数要素のグループ化

最終更新
2005-03-23T14:13:22+09:00

セクションをDIV要素でグループ化してみましょう。

<body>
<h1 />
<p />
<h2 />
<p />
</body>

上を次のようにグループ化します。単純化の為、BODY要素の内容全てをDIV要素でグループ化するというタスクを考えます。

<body>
<div>
  <h1 />
  <p />
  <h2 />
  <p />
</div>
</body>
Coreな方法
var div = document.createElement("DIV"),
    body = document.body;
while(body.lastChild)
    div.insertBefore(body.lastChild, div.firstChild);
body.appendChild(div);

かなり厄介です。浮動状態の新規DIV要素にBODY要素の子要素らを一つずつinsertBeforeし、最後にDIV要素をBODY要素内にappendChildします。

Rangeな方法
var range = document.createRange(),
    div = document.createElement("DIV");
range.selectNodeContents(document.body);
range.surroundContents(div);

一方こちらは、BODY要素の内容を選択し、その選択範囲を作成したDIV要素で括ってやるわけです。要素で括るってのが少し意味不明ですから、DIV要素としてマークアップする、と言い換えましょうか。タスクそのままの処理形式です。

タスク3 脱皮

最終更新
2005-03-23T19:51:12+09:00

DIVの皮を一枚脱ぎ捨てたい時。そんな時は御座いませんか?

<body>
  <div id="carapace">
    <h1 />
    <p />
  </div>
</body>
Coreな方法
var div = document.getElementById("carapace"),
    df = document.createDocumentFragment();
while(div.lastChild) {
  df.insertBefore(div.lastChild, df.firstChild)
}
div.parentNode.insertBefore(df, div);
div.parentNode.removeChild(div);

divの親だとか末子だとか、色んな連中がしゃしゃりでてきてウザいですね。

Rangeな方法
var df,
    div = document.getElementById("carapace"),
    range = document.createRange();
range.selectNodeContents(div); // divの内容を選択
df = range.extractContents(); // divの内容をdfとして抽出
range.setStartBefore(div); // 始点をdivの前に設定
range.insertNode(df); // 始点の位置にdfを挿入
range.selectNode(div); // divを選択
range.deleteContents(); // divを削除

いやー素晴らしい。親も末子も出てきませんよ。登場人物はdivとその内容を含むdfだけです。手続きがこんなに明確にコードで視認できるというのは感動です。本当に気に入りました。

タスク4 要素名の変更

最終更新
2005-03-23T21:18:24+09:00

H1要素をH2要素に変更したい場合、ありますか? あるんですこれが。DOM Level3 にはrenameNodeなんて便利なメソッドがあるらしいですが、無いものを使おうとしてもしかたありません。次のH1要素は、変数名h1で参照可能であるとします。

<h1><em>態と</em>面倒なツリー構造を持たせた</h1>
Coreな方法
var h2 = document.createElement("H2");
while(h1.firstChild)
  h2.appendChild(h1.firstChild);
h1.parentNode.insertBefore(h2, h1);
h1.parentNode.removeChild(h1);
Rangeな方法
var range = document.createRange(),
    h2 = document.createElement("H2");
range.selectNodeContents(h1);
h2.appendChild(range.extractContents());
range.setStartBefore(h1);
range.insertNode(h2);
range.selectNode(h1);
range.deleteContents();

この件に関してはどっこいどっこいでしょうか。でも要素の内容が巨大なら、Rangeの圧勝でしょう。