【JavaScript】範囲(Range)について図解で理解する。


# JavaScriptのRangeについて、よく分からなかったので、図を交えて理解してみました。
 
# Rangeについて

Rangeとはざっくり言うと、HtmlにおけるDOMの範囲を保持するオブジェクトです。

例えば、以下のようにDOMの範囲を指定したい時に使用します。
rangeを使用することにより、指定した部分を削除したり別のDOMに置き換えたりする事が可能となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/d74a9f2a-1404-4aa0-d523-ab5f81094685.png)

Rangeオブジェクトは以下で作成することができます。

```js
const range = new Range();
```

Rangeオブジェクトは、6つのプロパティーを持ちます。

1. startContainer
1. startOffset
1. endContainer
1. endOffset
1. collapsed
1. commonAncestorContainer

## ①startContainer、②startOffsetについて
startContainerとは範囲の開始のノードです。
startOffsetとは、startContainerのうちの開始ポイントです。
イメージとしては、startContainerでRangeの開始ノードをざっくりと指定して、startOffsetでstartContainerのうちの実際の開始点をピンポイントで示しているような感じです。

startContainer、startOffsetはRangeオブジェクトのsetStartメソッドで設定することができます。
第一引数に、startContainerに設定したいノードを、第二引数にstartOffsetの位置を数値で渡します。

```js
const range = new Range()
range.setStart(startContainer, startOffset);
```

今下記の様なhtmlが存在するとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/b61735b6-edf4-62a4-ff5b-ec7065ee5c9a.png)


```html
<!DOCTYPE html>
<html>

<head>
  <title>Document</title>
</head>

<body>
  <div class="content">
    <p id="hoge">ひとつめ<span class="second">ふたつめ</span><b>みっつめはふとじ<span>（こども） </span>だよ</b></p>
    <p id="fuga">いつつめ<span class="second">むっつめ</span><b>ななつめもふとじ<span>（ななつめのこども）</span>だよ</b></p>
  </div>
</body>

</html>

<style>
  .second {color: red;}
  b {color: green;}
  b span {color: blue}
</style>
```

今下記のようにrangeの開始点を設定するとします。

```js
<script>
  const hoge = document.getElementById("hoge")
  const range = new Range()
  range.setStart(hoge, 1);
</script>
```

この場合、以下の矢印のポイントがrangeの開始点となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/5a8d9347-cd0f-bd23-4826-e8e9afb7cb33.png)


setStartの第一引数でhogeがstartContainerに設定されました。
hoge内には3つのノード（①"ひとつめ"(テキストノード)、②\<span>、③\<b>）が存在します。
※  \<span>(こども)\</span>は\<b>のchildノードであるため、あくまでhogeが内包しているのは\<b>全体となる。

setStartの第二引数で"1"を指定したため、矢印の部分がrangeの開始点になります。

では、開始範囲を"ひとつめ"というテキストの"ひ"と"と"の間に開始点を設定したい場合はどのようにすればよいでしょうか？

Rangeのoffsetは、内包するノードがテキストノードのみの場合は、それぞれの文字の間にインデックスが入ります。

よって例えば、"ひとつめ"というテキストノードをstartContainerに設定し、startOffsetを1とすれば上記の開始点の設定が可能となります。

↓ こういうことです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/f8d8c388-0371-0bec-fd03-519fcd2254fb.png)

以下の様に設定します。

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge.childNodes[0], 1);
  //hoge.childNodes[0]で"ひとつめ"というテキストノードをstartContainerに設定。
  //hoge.childNodes[0]はテキストノードのため、文字の間にindexが指定される。
  //staretOffsetで"1"の位置を、開始点に設定する。
</script>
```

## ③endContainer、④endOffsetについて
endContainerとは範囲の終了のノードです。
endOffsetとは、endContainerのうちの終了ポイントです。
endContainer、endOffsetもstartContainer、startOffsetと同様に終点を設定します。

下記の矢印を終点にしたい場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/4936678b-306f-ccfc-2367-6abe68b9a14a.png)
下記の様に設定します。

```js
<script>
  const hoge = document.getElementById("hoge");
  const range = new Range();

  range.setEnd(hoge, 2);
</script>
```
↓  こういうことです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/fe0b88ab-ffc2-474f-9a0b-3751cb459edb.png)
それでは、下記のようにrangeを設定したい場合は、どのようにすればよいでしょうか？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/a6324fbd-fccd-f451-1e7a-af1f0f6cb978.png)

以下のようにRangeを設定します。

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge, 2);
  range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);
</script>
```

まず、下記で開始点を設定しています。

```js
range.setStart(hoge, 2);
```

下記のような状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/75d48762-e395-c648-960a-5c05ff27930d.png)

そして、下記で終了点を設定しています。

```js
range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);
```
少しわかりづらいですが、下記の様に設定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/2dace2a3-6aec-0d45-c1dd-b3c8e51f3610.png)

下記のようなRangeを設定することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/b46836e2-6568-5651-0132-181657a318cc.png)

## ⑤collapsedについて
collapsedは、rangeが潰れている(collapse)か否かをbooleanで保持しています。
潰れているとは、開始点と終了点が同じポイントであるということです。

先ほどの下記の例では、開始点と終了点は異なるのでcollapesedは"false"になります。

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge, 2);
  range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);

  console.log(range.collapsed);
  // false
</script>
```

下記のように開始点と終了点が同じ場合、collapesedは"true"となります。

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge, 2);
  range.setEnd(hoge, 2);

  console.log(range.collapsed);
  // true
</script>
```

また、**collapse**というメソッドで、開始点と終了点を同じ位置に合わせることができます。

引数にtureを与えた場合は、開始点に終了点を合わせる、falseを与えた場合は、開始点を終了点に合わせます。

## ⑥commonAncestorContainerについて
commonAncestorContainerは、range 内のすべてのノードの最も近い共通の祖先が格納されています。

下記のようにrangeを設定した場合

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge, 2);
  range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);

</script>
```
commonAncestorContainerは、\<div class="content">となります。
range内に含まれる、各ノードの親を順次辿っていくと、全てのノードで共通する最初の祖先は\<div class="content">になります。

## Rangeの持つメソッドについて
すでに紹介した、setStart、setEnd、collapseの他にも様々なメソッドをRangeは持っています。
***
### selectNode
selectNodeは引数にノードを取ります。
selectNodeは、Rangeを、引数に渡されたノードを囲む範囲にします。

```js
<script>
  const hoge = document.getElementById("hoge");
  const range = new Range()

  range.selectNode(hoge.childNodes[1])
</script>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/551f8a8a-adf3-fc9f-3b0a-9ed3bcd6e4b3.png)
***
### selectNodeContents
selectNodeContentsも引数にノードを取ります。
selectNodeとの違いは、selectNodeContentsは、Rangeを、そのノードの内側を範囲にします。

```js
<script>
  const hoge = document.getElementById("hoge");
  const range = new Range()

  range.selectNodeContents(hoge.childNodes[1])
</script>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/6e926ed8-c214-71f3-c535-abe3db70c64e.png)
***
### setStartBefore、setStartAfter、setEndBefore、setEndAfter
全て引数にノードを取ります。
**setStartBefore** : 引数のノードの直前をrangeの開始点に設定する。
**setStartAfter** : 引数のノードの直後をrangeの開始点に設定する。
**setEndBefore** : 引数のノードの直前をrangeの終了点に設定する。
**setEndAfter** : 引数のノードの直後をrangeの終了点に設定する。

下記の様にrangeを設定した場合、、、
```js
  <script>
    const hoge = document.getElementById("hoge");
    const fuga = document.getElementById("fuga");
    const range = new Range();
    range.setStartAfter(hoge.childNodes[1]);
    range.setEndBefore(fuga.childNodes[2])
  </script>
```
rangeは下記の通りとなります。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/398016ed-f317-2cff-8a68-a3b5d6390b5d.png)
***
### deleteContents
deleteContentsは、rangeの範囲をごっそり削除します。

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge, 2);
  range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);

  range.deleteContents();
</script>
```
下記のRangeの範囲が削除されて
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/b46836e2-6568-5651-0132-181657a318cc.png)
下記のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/c6d11af9-0d29-453a-a24a-15e0f41fdd9e.png)
また、削除された開始タグや終了タグは自動で補完してくれるので実際は下記のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/24195075-2166-e759-2fce-13ca1de78f44.png)

rangeは削除された範囲があった位置のままで、開始点と終了点は当然一致することとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/0116b217-d9ee-9333-6f57-329dcf4346dd.png)

***
### cloneContents
rangeの範囲をコピーしてその部分を返すメソッドです。返り値はDocumentFragmentとなります。
```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge, 2);
  range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);

  const clonedNode = range.cloneContents()
  console.log(clonedNode);  
</script>
```

```html
  console.log(clonedNode);  

↓ 　DocumentFragment

    <p id="hoge"><b>みっつめはふとじ<span>（こども）</span>だよ</b></p>
    <p id="fuga">いつつめ<span class="second">むっつめ</span><b>ななつめもふとじ<span>（ななつめのこど</span></b></p>
```
例によって開始タグや終了タグは自動で補完してくれます。

***
### extractContents
extractContentsは、deleteContentsとcloneContentsを同時に行ってくれます。
つまり、Rangeの範囲を削除し、削除した部分をDocumentFragmentで返します。
***
### insertNode
insertNodeは、ノードを引数にとり、そのRangeの開始点の位置に引数のノードを挿入します。

```js
</html>
<script>
  const hoge = document.getElementById("hoge");
  const range = new Range()

  range.setStart(hoge, 2);

  const newP = document.createElement("p")
  newP.textContent = "そうにゅうしたよ"
  range.insertNode(newP);
</script>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/0cc379e1-e7be-143a-5e53-0fff0049d588.png)

insertNodeはrangeの開始点のみが関係してくるため、範囲、および終了点は関係がありません。
***
### cloneRange
cloneRangeはrangeを複製します。返り値は複製されたRangeで、開始点・終了点は元のRangeと同じになります。
***
### toString
toStringは、toStringは、Rangeの範囲を全てテキストにして返すメソッドです。タグは無視されて、テキストノードのみが文字列化して返されます。

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");
  const range = new Range()

  range.setStart(hoge, 2);
  range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);

  const stringRange = range.toString();

  console.log(stringRange);

 //みっつめはふとじ（こども）だよ
 //いつつめむっつめななつめもふとじ（ななつめのこど
</script>
```
***
### compareBoundaryPoints
compareBoundaryPointsは2つのRangeの位置を比較するメソッドです。

第一引数に、「比較の仕方」、第二引数に「基準となるrange」を渡します。

比較の仕方は以下の4つの定数のどれかを渡します。

・START_TO_START : 開始点同士を比べる。
・START_TO_END : 基準ノードの開始点と、rangeの終了点を比べる。
・END_TO_START : 基準ノードの終了点と、rangeの開始点を比べます。
・END_TO_END　: 終了点どうしを比べます。

返り値は-1,0,1のいずれかの数で返します。
rangeが基準ノードより前なら-1、位置が同じなら0、後なら1が返ります。

下記の様に全く同じ範囲を持つrangeを二つ作成します。

```js
<script>
  const hoge = document.getElementById("hoge");
  const fuga = document.getElementById("fuga");

  const range = new Range();
  const rangeBase = new Range();

  range.setStart(hoge, 2);
  range.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);

  rangeBase.setStart(hoge, 2);
  rangeBase.setEnd(fuga.childNodes[2].childNodes[1].childNodes[0], 8);

  const result = range.compareBoundaryPoints(range.START_TO_START, rangeBase);

  console.log(result);
  // 0
</script>
```
開始点も当然同じなので、ログには 0 が出力されます。

以下の様に基準となるrange側のstartOffsetを前にずらして設定してみます。

```js
  rangeBase.setStart(hoge, 1);
```

```js
  const result = range.compareBoundaryPoints(range.START_TO_START, rangeBase);
  console.log(result);
  //1
```
rangeの開始点が、rangeBaseの開始点より後ろの位置にあるため"1"が出力されました。

rangeがbaseRangeに内包されているか否かを調べる場合は、下記で確認することができます。

```js
if(range.compareBoundaryPoints(range.START_TO_START, rangeBase) >= 0 
&& range.compareBoundaryPoints(range. END_TO_END, rangeBase) <= 0)

//  rangeの開始点が、rangeBaseの開始点より後ろ　
//  かつ 
//  rangeの開始点が、rangeBaseの開始点より前
```
rangeがbaseRangeに全く含まれていないかを調べる場合は、下記で確認することができます。

```js
if(range.compareBoundaryPoints(Range.START_TO_END, baseRange) <= 0
|| range.compareBoundaryPoints(Range.END_TO_START, baseRange) >= 0)

// rangeの終了点が、rangeBaseの開始点より前
// または
// rangeの開始点が、rangeBaseの終了点より後ろ 　
```