uhyohyo.net

JavaScript初級者から中級者になろう

八章第一回 Rangeとは

八章では、Rangeというものを解説します。今回は、そもそもRangeとはどういうものかを解説します。

Rangeとは

Rangeとは、「範囲」を表すものです。例えば、

あい<strong>うえ</strong>お。

というソースががあれば、

あいうえお。

のように表示されます。このとき、例えば

あいう  。
   ↑  ↑
  ここから  ここまで

のような感じである範囲を表せます。

要するに「『え』から『お』まで」なので、案外単純なように見えます。しかし、DOMという観点からこれを見ていくと、上の木構造は

|
├――#text  "あい"
|
├――strong
|   |
|   └――#text  "う"
|
└――#text  "。"
          

となっていて、複数のノードにまたがっています。こうなると、一筋縄ではいかないのが想像がつくと思います。このようなものを扱うのが、Rangeなのです。

範囲の表し方

まず、Rangeには「開始点」と「終了点」があります。そのままですね。

今回の場合、開始点は簡単に言うと「『え』の前」、終了点は「『。』の前」ということになりますね。しかし、こういう方法だとやはり曖昧です。そこで、DOMでは、ある点(開始点・終了点)を表すのに、コンテナオフセットを使います。

これらの意味は、実は、文字列を表す、テキストノードなどの場合と、それ以外の場合に分けられます。まず、それ以外の場合について見ていきます。例えば、

AAA
|
├――BBB
|
├――CCC
|
└――DDD
          

という木構造があったとき、開始点や終了点と成り得る場所は、

AAA
|     ←BBBの前
├――BBB
|     ←BBBとCCCの間
├――CCC
|     ←CCCとDDDの間
└――DDD
      ←DDDの後
          

が考えられます。このように、文字列中の位置だけでなく、兄弟ノードにおいての、ノードとノードの間の位置も点と成り得ます。タグで表すと

<AAA> <BBB /> <CCC /> <DDD /> </AAA>

という感じです。

さて、このとき、コンテナは、これらの兄弟の親ノードです。今回の場合、AAAですね。そして、オフセットは数値です。これは、最初の子ノードの前を0として、その次を1,その次を2,・・・と順番に番号をつけていったものです。つまり、

AAA ←これがコンテナ
|     ←0
├――BBB
|     ←1
├――CCC
|     ←2
└――DDD
      ←3
          

ということです。

さて、これで、ノードとノードの間の位置を点とする場合は、その表し方が分かりました。次に、文字列中の位置(文字と文字の間)の表し方を解説します。

EEE
|
├――#text  "あいうえお"
|
└――#text  "かきくけこ"
          

という木構造があって、例えば"あいうえお"のテキストノードの中に点をおきたいとします。このとき、コンテナそのテキストノードです。つまり、

EEE
|
├――#text  "あいうえお"   ←これ
|
└――#text  "かきくけこ"
            

ですね。

このとき、その文字列の中でも、

 あ い う え お
↑ ↑ ↑ ↑ ↑ ↑
0  1  2  3  4  5
            

これだけ可能性があります。数字が振ってありますが、これがそのままオフセットになります。つまり、一番最初の位置が0、その次が1,・・・という具合ですね。

ここで、

|
├――#text  "あい"
|
├――strong
|   |
|   └――#text  "う"
|
└――#text  "。"
            

の「えお」の部分を範囲とするRangeの、開始点と終了点を表わしてみましょう。まず、開始点のコンテナは

|
├――#text  "あい"
|
├――strong
|   |
|   └――#text  "うえ"   ←これ
|
└――#text  "お。"
            

で、

 う え
0 1 2

と考えると、オフセットは1であることが分かります。

同様に、終了点は、コンテナが

|
├――#text  "あい"
|
├――strong
|   |
|   └――#text  "うえ"
|
└――#text  "お。"   ←これ
            

で、オフセットは1ですね。

Rangeの作り方

これで、Rangeの範囲の表し方が分かりました。いよいよ、JavaScriptでのRangeの作り方を解説します。

Rangeを作るには、documentの持つメソッドcreateRangeを使います。引数はなく、戻り値で新しくできたRangeが帰ってきます。

var r = document.createRange();

ここでできたRangeの開始点と終了点は、共に「文書の一番前」ということになっています。コンテナとオフセットで表すと、コンテナはdocumentで、オフセットは0ということになっています。つまり、

document ←コンテナ
 |         ←ここ
 └――html
     |
     ├――body
     |   |
     |   …
     └――head
        |
        …
            

ですね。確かに一番前です。

さて、ただRangeを作っても、開始点や終了点がまともでなければ使い物になりません。そこで、開始点・終了点を変更するメソッドがあります。それぞれ、setStartsetEndです。

どちらも、引数は2つです。一つ目はコンテナ、二つ目がオフセットです。

var r = document.createRange();
r.setStart(document.body,0);
r.setEnd(document.body,document.body.childNodes.length);
            

という感じで使います。ちなみに、このソースでやっていることは分かりますか? 開始点は、body要素の一番最初に設定しています。終了点のコンテナも同じbody要素ですが、オフセットがdocument.body.childNodes.lengthとなっています。

これはどういうことかというと、あるノードの最初の子ノードを0番目の子ノードとしたとき、

AAA
|     ←オフセット:0
├――BBB	…0番目
|     ←オフセット:1
├――CCC	…1番目
|     ←オフセット:2
└――DDD	…2番目
      ←オフセット:3
            

となっていて、オフセットがnの位置は、n番目の子ノードの直前になっています。ここで、childNodes.lengthは子ノードの数なので、一番最後の子ノードは(length-1)番目ということになります。そして、最後の子ノードのさらに後ろに設定したい場合、オフセットを最後の子ノードの直前からさらに1増やすことになります。最後の子ノードの直前のオフセットは(length-1)だから、それに1を出してオフセットはlengthになります。

つまり、childNodes.lengthを指定することで、その要素の一番後ろに位置を設定していたのです。

結局、このRangeは、body要素の中身全体を包んでいるということになります。

また、開始点や終了点を設定するときの注意として、コンテナとなるノードは同じdocumentかdocumentFragmentの木構造に属していなければならないということがあります。考えてみれば、そもそも木構造に属していないノード同士や、片方しか属していないような状態で、その間の範囲なんてものが存在するわけがありません。

また、開始点が終了点より後ろになるように設定したり、終了点が開始点より前になるように設定することもできません。もしそのように設定しようとした場合、例えば開始点が終了点より後ろになるようにしようとした場合は、終了点もその場所まで押し下げられます。逆も然りです。ちなみに、このように、開始点と終了点が一致しているRangeを「つぶれている」(collapsed)というそうです。

だから、上のコードでは、createRangeで作ったノードは開始点と終了点が共に文書の一番前だからつぶれているRangeで、そのあとsetStartで開始点をbody要素の最初に設定しようとしたとき、終了点より開始点が後になってしまうから、一旦終了点もbody要素の最初の位置にきていたのです。

また、現在の開始点や終了点を知る方法もあります。これらは、Rangeのプロパティとして参照できます。startContainer,startOffset,endContainer,endOffsetの4つのプロパティをRangeは持っており、それぞれ開始点のコンテナ、開始点のオフセット、終了点のコンテナ、終了点のオフセットです。

ただし、これらは書き換えできません。これを直接いじって開始点・終了点を変えることはできないのです。

その他のメソッド・プロパティ

他にもいろいろRangeをいじるメソッドがあります。

setStartBefore,setEndBeforeは、それぞれ開始点・終了点を、あるノードの直前に移動します。引数はそのノードです。例えば、

var r = document.createRange();
r.setStartBefore(document.body);
            

とした場合、body要素の前だから、開始点は

html ←コンテナ
|
├――head
|   └…
|    ←ここ
└――body
    └…
            

ということになります。即ち、コンテナはhtml要素で、オフセットは1ですね。

また、setStartAfter,setEndAfterというメソッドもあります。これは、上の2つは逆にあるノードの直後に移動します。

さらに、使う機会があるかは知りませんが、collapseというメソッドがあります。これは、そのRangeをつぶれた状態にする、つまり開始点と終了点が同じ状態にするものです。このとき、開始点に合わせるか終了点に合わせるかという問題があるので、それを1つの引数で指定します。これは真偽値で、trueなら開始点、falseなら終了点に合わされます。

また、そのRangeが今つぶれているかどうかは、collapsedというプロパティに真偽値で入っています。

さらにさらに、selectNodeselectNodeContentsという2つのメソッドがあります。どちらも引数はノード一つです。これらのメソッドでは、開始点と終了点が同時に変更されます。

selectNodeは、Rangeを、そのノードを囲む(選択した)状態にするというものです。例えば、

var r = document.createRange();
r.selectNode(document.body);
            

としたとき、

html ←コンテナ
|
├――head
|   └…
|    ←開始点
└――body
    └…
     ←終了点
            

となります。body要素全体が範囲になっていることがわかります。コンテナは、いずれもhtml要素ですね。

selectNodeContentsは、そのノードの外側から囲むのではなく、内側から囲みます。つまり、例えば

var r = document.createRange();
r.selectNodeContents(document.body);
            

としたとき、

html
|
├――head
|   └…
|
└――body ←コンテナ
    |  ←開始点
    ├―…
    |
    ├―…
    |
    └―…
      ←終了点
            

という感じになります。コンテナはどちらもbody要素になります。

今回は、Rangeの作り方を解説しました。次回から、Rangeの使い方を解説していきます。