【JavaScript】Selectionについて図解で理解する。

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

## Selectionとは
Selectionとは、ざっくり言ってしまうとユーザーが選択した範囲を表すものです。
前回の[Rangeの説明](https://qiita.com/sho_U/items/088d15d38176b82dce00)では、Rangeの範囲をプログラム上で設定していましが、Selectionを使用する事により、ユーザーが選択した範囲をRangeに設定する事ができます。

下記のコードで、ユーザーの選択範囲からRangeオブジェクトを取得することが可能です。
```js
 const selection = document.getSelection();
 const range = selection.getRangeAt(0);
```
まず、下記でSelectionオブジェクトを取得します。
```js
 const selection = document.getSelection();
```

次に、下記のコードからselectionオブジェクトの１番目(インデックス0)からRangeオブジェクトを取得しています。
```js
 const range = selection.getRangeAt(0);
```
なぜ、１番目を指定する必要があるのか？
FireFoxのように、複数選択できるブラウザの場合に、どの選択範囲かを指定するためです。

![b4e055fc1e9401b9dde8a5cf6306f4d6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/db15c275-766d-b9db-56c4-4d06f38f97af.gif)
chromeの場合、複数選択することはできませんので、実質的に **selection.getRangeAt(0)** で取得する形となります。

## 下記図のように、選択範囲を削除する実装を行ってみます。
![8898c343ac809359102b457638887211.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/3a812ef9-188c-36cd-bb9a-9ecba2861f70.gif)　　

```html
<!doctype html>
<html>

<head>
  <title>test</title>
</head>

<body>
  <div id="words">
    <p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
    <p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
    <p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
  </div>
  <button onclick="deleteNode()">削除</button>

  <script type="text/javascript">
    let range;

    document.addEventListener('mouseup', function (ev) {
      const selection = document.getSelection();
      range = selection.getRangeAt(0);
    }, false);

    function deleteNode() {
      range.deleteContents();
    }
  </script>
</body>
</html>
```

下記のコードで、マウスボタンが離された時に、Selectionオブジェクトを作成し、Rangeオブジェクトを取得しています。

```js
document.addEventListener('mouseup', function (ev) {
      const selection = document.getSelection();
      range = selection.getRangeAt(0);
    }, false);
```

次に、ボタンをクリックした場合に、rangeのノードを削除する関数を定義しています。
deleteContentsについて分からない場合は、[Rangeの説明](https://qiita.com/sho_U/items/088d15d38176b82dce00#deletecontents)をご確認ください。
```js
    function deleteNode() {
      range.deleteContents();
    }
```

今回はイベントハンドラとして"mouseup"を使用しましたが、選択範囲自体によるイベントハンドラも存在します。

1. **selectstart** : ユーザがボタンを押しながらマウスを動かし始めた時など、選択が開始された時に発火。
1. **selectionchange** : 選択範囲が変更された時(documentに対してのみ設定可能)

## addRangeメソッド
逆にrangeオブジェクトからSelectionオブジェクトを設定することも可能です。
addRangeを使用します。
引数には、Rangeオブジェクトを渡します。

```html
<body>
  <div id="words">
    <p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
    <p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
    <p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
  </div>

  <button onclick="setSelection()">選択範囲設定</button>

  <script type="text/javascript">
    const range = new Range();
    const words = document.getElementById("words");
    range.setStart(words.children[0].childNodes[0], 3)
    range.setEnd(words.children[2].childNodes[0], 5)

    function setSelection() {
      const sel = document.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);
    }
  </script>
</body>
```
「選択範囲設定」をクリックすると、下記の図のようにrangeの範囲が選択された状態となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/1112efb5-033e-4caf-d1da-36b4aaab9cde.png)

まずは、下記でrangeで範囲を設定しています。
rangeの範囲の設定について分からない場合は、[Rangeの説明](https://qiita.com/sho_U/items/088d15d38176b82dce00#range%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)をご確認ください。

```js
    const range = new Range();
    const words = document.getElementById("words");
    range.setStart(words.children[0].childNodes[0], 3)
    range.setEnd(words.children[2].childNodes[0], 5)
```

下記関数で選択範囲にrangeを追加しています。
Firefox以外のブラウザは選択範囲がすでに存在する場合、新しいrangeを無視します。
ですので、**sel.removeAllRanges()** で、範囲を一度空にしています。
**sel.addRange(range)** でrangeをselectionに追加します。

```js
function setSelection() {
      const sel = document.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);
    }
```

## Selectionのプロパティー
SelectionもRangeと類似のプロパティーを持っています。

1. **anchorNode** : selectionの開始ノードです。
2. **anchorOffset** : anchorNodeでの開始点となるオフセットです。
3. **focusNode** : selectionの終了ノードです。
4. **focusOffset** : focusNodeでの終了点となるオフセットです。
5. **isCollapsed** : selectionが未選択(空の範囲) あるいは存在しない場合 true になります。
6. **rangeCount** : selectionに含まれるRangeオブジェクトの数です。

下記のようにselectionにrangeを設定した場合、当然rangeとselectionの範囲が同じとなるので、以下のログは全てtrueとなります。
```js
    function setSelection() {
      const sellection = document.getSelection();
      sellection.removeAllRanges();
      sellection.addRange(range);
      console.log(range.startContainer === sellection.anchorNode); //true
      console.log(range.startOffset === sellection.anchorOffset); //true
      console.log(range.endContainer === sellection.focusNode); //true
      console.log(range.endOffset === sellection.focusOffset); //true
    }
```

Rangeオブジェクトの場合、開始点が終了点より手前に来ることはありませんが、selectionの場合はanchorよりfocusが手前に来ることがあります。
![20cb0d069f23ddc3e810f33842ea9530.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/12064b63-48fe-1424-d827-f4831bc62cb3.gif)
上記の様に、逆から選択してみます。

```html
<body>
  <div id="words">
    <p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
    <p>1234567890</p>
    <p>あいうえおかきくけこ</p>
  </div>

  <button onclick="setSelection()">選択範囲設定</button>

  <script type="text/javascript">
    function setSelection() {
      const sel = document.getSelection();
      console.log("anchorNode", sel.anchorNode);  //"あいうえおかきくけこ"
      console.log("focusNode", sel.focusNode);  //"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    }
  </script>
</body>
```
focusNodeがanchorNodeより手間に来ていることがわかります。
anchorが終点focusの前にある場合、この選択は "正" 方向と呼ばれます。
逆の場合は"負"の方向と呼ばれます。

## 選択範囲を直接設定する。
selectionは、addRangeでrangeを追加せずとも直接範囲を指定することも可能です。

### collapseメソッド　(エイリアス : setPositionメソッド)
第一引数にノード。第二引数にoffsetの値を取ります。引数のノードのoffsetの位置に開始及び終了点を設定します。

### collapseToStartメソッド
現在の選択範囲の開始点に終了点を合わせます。引数はありません。

### collapseToEndメソッド
現在の選択範囲の終了点に開始点を合わせます。引数はありません。

### extendメソッド
第一引数にノード。第二引数にoffsetの値を取ります。
選択範囲のfocusを引数のノードのoffsetの位置に移動します。

### setBaseAndExtentメソッド
第一引数にanchorに設定したいノード。第二引数にoffsetの値、第三引数にfocusに設定したいノード。第四引数にoffsetの値を設定します。
引数に渡されたされた開始点 anchorNode（第一引数）/anchorOffset(第二引数) と終了点 focusNode(第三引数)/focusOffset(第四引数) に設定します。

### selectAllChildrenメソッド
引数にノードを設定します。引数に渡されたノードの全ての子ノードを選択範囲に設定します。

### deleteFromDocumentメソッド
ドキュメントから選択された範囲を削除します。引数はありません。

### containsNodeメソッド
第一引数にノード、第二引数にbooleanを渡します。
選択範囲が第一引数のノードを含むかチェックします。含む場合はtrue、含まない場合はfalseを返します。
第二引数が true の場合は部分的に含む場合も、含むと判断してtrueを返します。
第二引数が false の場合は、ノードが選択範囲に完全に含まれている場合にのみ、trueを返します。
第二引数を省略した場合は、デフォルトでfalseとなります。