はじめに
DOMはWebページを処理するための非常に優れたAPIですが、一般的には標準のDOM機能ばかりが注目されています。多くの開発者は、DOMにただのcreateElement()やappendChild()よりもっと便利な機能があることを知りません。DOMのRange(レンジ)は、Webページを動的に操作するための非常に強力なツールです。
レンジを使用すると、ドキュメントのセクションをノード境界に関係なく選択できます(この選択は内部的に行われるため、ユーザーには見えません)。レンジは、通常のDOM操作では処理できないドキュメントの細部を変更したいときに役立ちます。
DOM Level 2では、レンジを作成するcreateRange()というメソッドが定義されています。DOM対応のブラウザ(Internet ExplorerはDOM対応ではありません)では、このメソッドはdocumentオブジェクトに属しているので、次のようにして新しいレンジを作成できます。
var oRange = document.createRange();
ノードと同様に、レンジはドキュメントに直接結び付いています。ドキュメントがDOMスタイルのレンジに対応しているかどうかを判断するには、hasFeature()メソッドを使用します。
var supportsDOMRanges = document.implementation.hasFeature("Range", "2.0");
DOMのレンジを使用する場合には、まずこの点を確認し、次のようにコードをif文で囲んでおくことをお勧めします。
if (supportsDOMRange) {
var oRange = document.createRange();
//range code here
}
DOMレンジによる単純な選択
DOMレンジを使ってドキュメントの一部を選択する最も単純な方法は、selectNode()またはselectNodeContents()を使用することです。これらのメソッドは、1つの引数(DOMノード)を受け取り、そのノードから取得した情報をレンジに割り当てます。selectNode()メソッドは指定されたノード全体(子を含む)を選択しますが、selectNodeContents()は指定されたノードのすべての子を選択します。たとえば、次のようなドキュメントがあるとします。
<p id="p1"><b>Hellob> Worldp>
このドキュメントに対して、次のJavaScriptを実行したとしましょう。
var oRange1 = document.createRange();
var oRange2 = document.createRange();
var oP1 = document.getElementById("p1");
oRange1.selectNode(oP1);
oRange2.selectNodeContents(oP1);
この例の2つのレンジには、ドキュメント内のそれぞれ異なるセクションが含まれます。oRange1には要素とそのすべての子が含まれ、oRange2には要素とテキストノード「World」が含まれます(図1を参照)。
レンジを作成すると、そのレンジには次のようなプロパティが割り当てられます。
- startOffset
レンジの開始位置を含んでいるstartContainer内でのオフセット。startContainerがテキストノード、コメントノード、またはCDataノードである場合、このオフセットは、レンジを開始するまでにスキップする文字の数を表します。それ以外の場合は、レンジ内の最初の子ノードのインデックスを表します。
これらのプロパティはすべて読み取り専用で、レンジについての補足情報を提供するために用意されています。
selectNode()を使用した場合、startContainer、endContainer、commonAncestorContainerの各プロパティは、いずれも指定ノードの親ノードに等しくなります。また、startOffsetプロパティは指定ノードの親のchildNodesコレクションにおける指定ノードのインデックスに等しくなり、endOffsetプロパティは「startOffsetの値+1」に等しくなります(1つのノードだけが選択されるため)。
selectNodeContents()を使用した場合、startContainer、endContainer、commonAncestorContainerの各プロパティは、いずれも指定ノードに等しくなります。また、startOffsetプロパティは0になり、endOffsetは子ノードの数(node.childNodes.length)に等しくなります。
これらのプロパティの使用例を次に示します。
<html>
<head>
<title>DOM Range Exampletitle>
<script type="text/javascript">
function useRanges() {
var oRange1 = document.createRange();
var oRange2 = document.createRange();
var oP1 = document.getElementById("p1");
oRange1.selectNode(oP1);
oRange2.selectNodeContents(oP1);
document.getElementById("txtStartContainer1").value
= oRange1.startContainer.tagName;
document.getElementById("txtStartOffset1").value =
oRange1.startOffset;
document.getElementById("txtEndContainer1").value =
oRange1.endContainer.tagName;
document.getElementById("txtEndOffset1").value =
oRange1.endOffset;
document.getElementById("txtCommonAncestor1").value =
oRange1.commonAncestorContainer.tagName;
document.getElementById("txtStartContainer2").value =
oRange2.startContainer.tagName;
document.getElementById("txtStartOffset2").value =
oRange2.startOffset;
document.getElementById("txtEndContainer2").value =
oRange2.endContainer.tagName;
document.getElementById("txtEndOffset2").value =
oRange2.endOffset;
document.getElementById("txtCommonAncestor2").value =
oRange2.commonAncestorContainer.tagName;
}
script>
head>
<body><p id="p1"><b>Hellob> Worldp>
<input type="button" value="Use Ranges" onclick="useRanges()" />
<table border="0">
<tr>
<td>
<fieldset>
<legend>oRange1legend>
Start Container:
<input type="text" id="txtStartContainer1" /><br />
Start Offset:
<input type="text" id="txtStartOffset1" /><br />
End Container:
<input type="text" id="txtEndContainer1" /><br />
End Offset:
<input type="text" id="txtEndOffset1" /><br />
Common Ancestor:
<input type="text" id="txtCommonAncestor1" /><br />
fieldset>
td>
<td>
<fieldset>
<legend>oRange2legend>
Start Container:
<input type="text" id="txtStartContainer2" /><br />
Start Offset:
<input type="text" id="txtStartOffset2" /><br />
End Container:
<input type="text" id="txtEndContainer2" /><br />
End Offset:
<input type="text" id="txtEndOffset2" /><br />
Common Ancestor:
<input type="text" id="txtCommonAncestor2" /><br />
fieldset>
td>
tr>
table>
body>
html>
この例をFirefoxなどのDOM対応ブラウザで実行した結果を図2に示します。
図を見てもわかるように、oRange1のstartContainer、endContainer、commonAncestorContainerの各プロパティはいずれも要素になります。これは、この要素は完全に要素に含まれているからです。また、この要素の最初の子は要素なのでstartOffsetプロパティは0になり、このレンジは2つ目の子ノード(インデックス1)の前で終わるのでendOffsetプロパティは1になります。
一方、selectNodeContents()メソッドで取得したoRange2の情報では、startContainer、endContainer、commonAncestorContainerの各プロパティはいずれも要素になります。これは、このレンジでは要素の子を選択しているからです。また、このレンジは要素の最初の子ノードから始まっているのでstartOffsetプロパティは0になり、このレンジには要素の2つの子ノード(とテキストノード「World」)が含まれているのでendOffsetプロパティは2になります。
さらに、選択レンジをより細かく指定するために、次のようなメソッドが用意されています。これらのメソッドを使用すると、前述のプロパティには自動的に値が割り当てられます。
- setStartBefore(refNode)
レンジの開始位置をrefNodeの前に設定します(したがって、refNodeは選択内の最初のノードになります)。startContainerプロパティはrefNodeの親に等しくなり、startOffsetプロパティはrefNodeの親のchildNodesコレクションにおけるrefNodeのインデックスに等しくなります。
- setStartAfter(refNode)
レンジの開始位置をrefNodeの後ろに設定します(したがって、refNodeは選択内に含まれなくなり、その次の兄弟が選択内の最初のノードになります)。startContainerプロパティはrefNodeの親に等しくなり、startOffsetプロパティは「refNodeの親のchildNodesコレクションにおけるrefNodeのインデックス+1」に等しくなります。
- setEndBefore(refNode)
レンジの終了位置をrefNodeの前に設定します(したがって、refNodeは選択内に含まれなくなり、その前の兄弟が選択内の最後のノードになります)。endContainerプロパティはrefNodeの親に等しくなり、endOffsetプロパティはrefNodeの親のchildNodesコレクションにおけるrefNodeのインデックスに等しくなります。
- setEndAfter(refNode)
レンジの終了位置をrefNodeの後ろに設定します(したがって、refNodeは選択内の最後のノードになります)。endContainerプロパティはrefNodeの親に等しくなり、endOffsetプロパティは「refNodeの親のchildNodesコレクションにおけるrefNodeのインデックス+1」に等しくなります。
これらのメソッドを使用すると、前述のプロパティに値が自動的に割り当てられます。複雑なレンジ選択を行いたい場合には、各プロパティに値を直接指定することもできます。
DOMレンジによる複雑な選択
複雑なレンジを作成するには、レンジのsetStart()メソッドとsetEnd()メソッドを使用する必要があります。どちらのメソッドも、参照ノードとオフセットという2つの引数を取ります。setStart()では、参照ノードはstartContainerに割り当てられ、オフセットはstartOffsetに割り当てられます。setEnd()の場合、参照ノードはendContainerに割り当てられ、オフセットはendOffsetに割り当てられます。
これらのメソッドを使用して、selectNode()およびselectNodeContents()と同等の機能を実現することができます。たとえば、前の例に出てきたuseRanges()関数を、setStart()とsetEnd()を使って次のように書き換えることができます。
function useRanges() {
var oRange1 = document.createRange();
var oRange2 = document.createRange();
var oP1 = document.getElementById("p1");
var iP1Index = -1;
for (var i=0; i < oP1.parentNode.childNodes.length; i++) {
if (oP1.parentNode.childNodes[i] == oP1) {
iP1Index = i;
break;
}
}
oRange1.setStart(oP1.parentNode, iP1Index);
oRange1.setEnd(oP1.parentNode, iP1Index + 1);
oRange2.setStart(oP1, 0);
oRange2.setEnd(oP1, oP1.childNodes.length);
//textbox assignments here
}
ノードを選択する(oRange1)には、まず目的のノード(oP1)について、その親ノードのchildNodesコレクションにおけるインデックスを特定する必要があります。ノードの内容を選択する(oRange2)には、特に計算は必要ありません。もっとも、既にご存知のとおり、ノードやノードの内容を選択するにはもっと簡単な方法が他にあります。この方法のポイントは、ノードの一部だけを選択できるということです。
たとえば、Hello World
というHTMLコードから、「Hello」の「llo」だけと「World」の「Wo」だけを選択したい場合を考えてみましょう。この方法を使えば、これを簡単に実現できます。
まず、通常のDOMメソッドを使用して、「Hello」と「World」を含む2つのテキストノードの参照を取得します。
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
テキストノード「Hello」は、実際にはの孫にあたるので(直接の親は)、oP1.firstChildによってを取得し、oP1.firstChild.firstChildによって目的のテキストノードを取得します。テキストノード「World」はの2番目の(かつ最後の)子なので、oP1.lastChildによって取得できます。
次に、レンジを作成して適切なオフセットを設定します。
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
setStart()ではオフセットを2に設定します。これは、「Hello」中の1つ目の「l」は位置2にあたるからです(先頭の文字「H」は位置0です)。setEnd()ではオフセットを3に設定します。このオフセットには、選択しない最初の文字の位置を指定するので、「r」が登場する位置3を指定しています(図3に示すように、実は位置0のところにスペースが入っています)。
oHelloとoWorldはどちらもテキストノードなので、これらのノードはこのレンジのstartContainerとendContainerに割り当てられます。これにより、startOffsetとendOffsetは、要素が渡されたときのように子ノードを探すのではなく、各ノードに含まれているテキストを探すようになります。commonAncestorContainerは、両方のノードを含んでいる最初の祖先である要素になります。
当然ながら、ドキュメントのセクションを選択しただけでは何の役にも立たないので、この選択範囲に対して何らかの操作を行うことになります。以降では、この点について説明します。
DOMレンジの内容の操作
レンジを選択すると、内部的にドキュメントフラグメントノードが作成され、そこに選択内のすべてのノードがアタッチされます。ただし、この処理が行われる前に、選択内容が整形式であることが要求されます。
先ほどの例に示したように、この方法では、途中に終了タグを含んでいる、「Hello」の1つ目の「l」から「World」の「o」までの範囲を選択することができます(図4を参照)。このような選択は、『Professional JavaScript for Web Developers』(Nicholas C. Zakas 著、Wrox Pr Inc.、2005年4月)で説明されている通常のDOMメソッドでは不可能です。
レンジを使ったときにこの制限を回避できるのは、レンジでは欠けている開始タグと終了タグが認識されるからです。前述の例では、選択内に開始タグが足りないと判断されたため、レンジによって開始タグが動的に追加され、さらに「He」を閉じるための終了タグが新たに追加されます。これにより、DOMは次のようになります。
<p><b>Heb><b>llob> Worldp>
このレンジに含まれるドキュメントフラグメントは図5のようになります。
このドキュメントフラグメントが作成されると、レンジの内容をさまざまなメソッドで操作できるようになります。
最もわかりやすく使いやすいのはdeleteContents()メソッドでしょう。このメソッドでは、レンジの内容をドキュメントから削除します。前述の例でレンジに対してdeleteContents()を呼び出すと、ページ内のHTMLは次のようになります。
ドキュメントフラグメント全体が削除されるので、そのままだとタグが足りなくなりますが、レンジによってタグが補われるため、ドキュメントは整形式に保たれます。
extractContents()メソッドはdeleteContents()によく似ています。このメソッドでは、選択範囲をドキュメントから削除し、その範囲のドキュメントフラグメントを戻り値として返します。これにより、そのレンジの内容を他の場所に挿入することが可能になります。
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
var oFragment = oRange.extractContents();
document.body.appendChild(oFragment);
この例では、ドキュメントフラグメントを抽出した後、ドキュメントの要素の末尾に追加しています((ドキュメントフラグメントをappendChild()に渡したときは、そのフラグメント自身ではなく、フラグメントの子だけが追加されるので注意してください)。この例を実行すると、ページの先頭にはHerldが、ページの末尾にはllo Woが配置されます。
また、cloneContents()メソッドを使用すると、ドキュメントフラグメントをその場に残したまま、そのフラグメントのコピーを作成して、ドキュメントの他の場所に挿入することができます。
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
var oFragment = oRange.cloneContents();
document.body.appendChild(oFragment);
このメソッドはextractContents()によく似ています(どちらもレンジのドキュメントフラグメントを返します)。この例を実行すると、ページの末尾にllo> Woが追加され、元のHTMLコードはそのまま残ります。
筆者注
ドキュメントフラグメントとそのレンジ選択に対する変更は、上記のいずれかのメソッドが呼び出されるまでは発生しません。その時点までは、元のHTMLには何の変化もありません。
DOMレンジの内容の挿入
前述の3つのメソッドでは、それぞれ異なる方法でレンジの情報を削除またはコピーしました。また別のメソッドを使用して、レンジに内容を追加することもできます。
insertNode()メソッドでは、選択レンジの先頭にノードを挿入できます。たとえば、前のセクションで定義したレンジに次のHTMLコードを挿入することを考えてみましょう。
<span style="color: red">Inserted textspan>
この処理は次のコードで実現できます。
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
var oSpan = document.createElement("span");
oSpan.style.color = "red";
oSpan.appendChild(document.createTextNode("Inserted text"));
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
oRange.insertNode(oSpan);
このJavaScriptを実行すると、次のHTMLコードが生成されます。
<p id="p1"><b>He<span style="color: red">Inserted textspan>llob>
Worldp>
レンジ選択の前半部分である「Hello」の「llo」の直前にが挿入されていることに注目してください。また、元のHTMLで要素が追加も削除もされていないことに注意してください。これは、前のセクションで紹介したメソッドを使用していないからです。このテクニックは、役に立つ情報をページに挿入するときなどに利用できます(たとえば、新規ウィンドウで開くリンクを示すアイコンなど)。
レンジに内容を挿入するだけでなく、surroundContents()メソッドを使用して、レンジを囲むように内容を挿入することもできます。このメソッドは、レンジの内容を囲むノードを引数として受け取ります。内部的には、次のような処理が行われます。
- レンジの内容が抽出されます(extractContents()と同様)。
- 元のドキュメント内で、レンジが置かれていた位置に指定のノードが挿入されます。
- ドキュメントフラグメントの内容が指定のノードに追加されます。
この機能は、Webページ内の特定の単語を強調表示するときなどに役立ちます。次に例を示します。
var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
var oSpan = document.createElement("span");
oSpan.style.backgroundColor = "yellow";
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
oRange.surroundContents(oSpan);
このコードを実行すると、選択レンジの背景が黄色で強調表示されます。
DOMレンジの折りたたみ
レンジを空にする(つまりドキュメントのどの部分も選択していない状態にする)には、レンジの折りたたみ(collapse)を行います。レンジの折りたたみは、テキストボックスの動作に似ています。テキストボックスにテキストが含まれている場合、マウスを使用して単語全体を強調表示することができます。しかし、もう一度マウスの左ボタンをクリックすると、選択が解除され、2つの文字の間にカーソルが置かれます。レンジの折りたたみを行うときは、ドキュメントのパーツ間の位置を指定します(具体的には、レンジ選択の先頭または末尾になります)。図6に、レンジの折りたたみを行ったときの様子を示します。
レンジを折りたたむにはcollapse()メソッドを使用します。このメソッドは、折りたたみの方向を示すブール型の引数を1つ取ります。引数がtrueの場合はレンジの先頭に向けて折りたたみ、falseの場合はレンジの末尾に向けて折りたたみます。レンジが折りたたまれているかどうかを判別するには、collapsedプロパティを使用します。
oRange.collapse(true); //collapse to the starting point
alert(oRange.collapsed); //outputs "true"
レンジが折りたたまれているかどうかのテストは、レンジ内の2つのノードが隣り合っているかどうかを確認するときに役立ちます。たとえば、次のようなHTMLコードがあるとします。
<p id="p1">Paragraph 1p><p id="p2">Paragraph 2p>
このコードの正確な構造がわからない場合は(たとえばコードを自動生成した場合など)、次のようにしてレンジを作成してみます。
var oP1 = document.getElementById("p1");
var oP2 = document.getElementById("p2");
var oRange = document.createRange();
oRange.setStartAfter(oP1);
oRange.setStartBefore(oP2);
alert(oRange.collapsed); //outputs "true"
この例では、p1の末尾とp2の先頭の間に何もないので、作成したレンジは折りたたまれています。
DOMレンジの比較
複数のレンジがある場合は、compareBoundaryPoints()メソッドを使用して、それらのレンジに共通の境界(開始または終了)が含まれていないかどうかを確認できます。このメソッドは2つの引数を取り、これらの引数で比較対象のレンジと比較方法を指定します。比較方法は次の定数値で指定します。
| 定数値 | 定数名 | 挙動 |
| 0 | START_TO_START | 1つ目のレンジの開始点と2つ目のレンジの開始点を比較します |
| 1 | START_TO_END | 1つ目のレンジの開始点と2つ目のレンジの終了点を比較します |
| 2 | END_TO_END | 1つ目のレンジの終了点と2つ目のレンジの終了点を比較します |
| 3 | END_TO_START | 1つ目のレンジの終了点と2つ目のレンジの開始点を比較します |
compareBoundaryPoints()メソッドは、1つ目のレンジの境界点が2つ目のレンジの境界点よりも前にある場合は-1を返し、2つの境界点が等しい場合は0を返し、1つ目のレンジの境界点が2つ目のレンジの境界点よりも後にある場合は1を返します。
次に例を示します。
var oRange1 = document.createRange();
var oRange2 = document.createRange();
var oP1 = document.getElementById("p1");
oRange1.selectNodeContents(oP1);
oRange2.selectNodeContents(oP1);
oRange2.setEndBefore(oP1.lastChild);
alert(oRange1.compareBoundaryPoints(Range.START_TO_START, oRange2));
//outputs 0
alert(oRange1.compareBoundaryPoints(Range.END_TO_END, oRange2));
//outputs 1;
このコードでは、2つのレンジがどちらもselectNodeContents()のデフォルト値を使用しているので、2つの境界点がまったく同じ位置にあります。したがって、1つ目のcompareBoundaryPoints()メソッドは0を返します。しかし、oRange2については後からsetEndBefore()を使用して終了点を変更し、oRange1の終了点がoRange2の終了点よりも後に来るようにしているので(図7を参照)、2つ目のcompareBoundaryPoints()メソッドは1を返します。
DOMレンジの複製
必要に応じて、cloneRange()メソッドを使用してレンジを複製することができます。このメソッドは、呼び出したレンジの正確な複製を作成します。
var oNewRange = oRange.cloneRange();
新しいレンジには元のレンジとまったく同じプロパティが含まれており、これらのプロパティの値は、元のレンジに影響を与えずに修正できます。
クリーンアップ
レンジを使い終わったら、detach()メソッドを呼び出してシステムリソースを解放することをお勧めします。これは必須の作業ではなく、参照されなくなったレンジは最終的にガベージコレクタによって回収されます。しかし、レンジを使い終わって不要になったらdetach()を呼び出すようにすると、余計なメモリを占有せずに済みます。
この記事はNicholas C. Zakas著『Professional JavaScript for Web Developers』(Wrox, 2006, ISBN: 0-471-77778-1)の第10章「Advanced DOM Techniques」からの抜粋です。
Copyright 2005 by WROX. All rights reserved. Reproduced here by permission of the publisher.
Nicholas C. Zakas(Nicholas C. Zakas)