[JavaScript] contentEditable な要素のキャレット位置 (Range) 制御

2018年9月17日月曜日

javascript Range 選択制御

t f B! P L

enter image description here

[JavaScript] contentEditable な要素でキャレット位置 (Range) を指定する

JavaScript で選択範囲を操作するには

↓の実行結果のイメージのように、今日の定食はとても美味しかった。の部分を選択する場合の例です。

[実行結果]
実行結果

[HTML]

<div id="sample" contentEditable="true">
  <div id="line1">今日の定食はとても美味しかった。</div>
  <div id="line2">明日は<big>かつ丼定食</big>だ。</div>
</div>

[JS]

var selection = window.getSelection();    //Selectionインスタンスの取得
var range = document.createRange();       //Rangeインスタンスを生成する

//選択範囲の開始・終了位置を設定する
let  el  =  document.getElementById("sample");
range.setStart(el, 1);  //①
range.setEnd(el, 2);    //②

//現在の選択範囲をすべて解除
selection.removeAllRanges(); 

//上の処理で作成した選択範囲(range)を追加する
selection.addRange(range);

[解説]

ポイントとなるのが、上の①②のsetStart() setEnd()関数に渡す引数です。
この関数は引数を2とり、以下の内容を指定します。

  • 第1引数 ・・・ 選択範囲を開始/終了するNodeを指定
  • 第2引数 ・・・ 開始/終了Nodeからの子ノード数をOffset値として指定 (*)

*第2引数について補足
今回のサンプルJSコードのように、div#sample を第1引数に指定した場合、
setStart() setEnd()の第2引数に指定するOffset値は以下のような関係になります。

 ├ div#sample (Offset=0)
 │  └ div#line1 (Offset=1)            ← ①で指定される開始位置
 │  │   └ "今日の定食はとても美味しかった。"
 │  └ div#line2 (Offset=2)            ← ②で指定される終了位置
 │     └ "明日は"
 │     └ big
 │     │ └ "かつ丼定食"
 │     └ "だ。"

上述までの仕様である事から、
今回 ①から②の手前までの今日の定食はとても美味しかった。が選択されました!!

Offset 値に孫要素は指定できない

次は今日の定食はとても美味しかった。明日は の部分を選択する例です。
setStart() setEnd()の第2引数のOffset値には、孫要素の位置を指定出来ません。
そのため少し JavaScript のソースを修正します。

[実行結果]
実行結果その2

[JS]

// ~~ 省略 ~~

// 選択範囲開始を、div#line1 に設定
let  elStart  =  document.getElementById("sample");
range.setStart(elStart, 1);      //①
// 選択範囲終了を、bigタグの前に設定
let  elEnd  =  document.getElementById("line2");
range.setEnd(elEnd, 1);          //②

// ~~ 省略 ~~

[解説]

setStart() setEnd()の第2引数のOffset値には、孫要素の位置を指定出来ません。
そのため、子要素の Offset 値で指定するように、
setStart() setEnd()の第1引数にそれぞれ異なるノードを指定しています。
その際の offset値の関係性は、それぞれ以下の通りとなります。

  • Range.setStart()
 ├ div#sample (Offset=0)
 │  └ div#line1 (Offset=1)            ← ①で指定される開始位置
 │  │   └ "今日の定食はとても美味しかった。"
 │  └ div#line2 (Offset=2)
 │    ・・・
  • Range.setEnd()
 ├ div#sample
 │  └ div#line1       
 │  │   └ "今日の定食はとても美味しかった。"
 │  └ div#line2 (Offset=0)
 │     └ "明日は"
 │     └ big (Offset=1)            ← ②で指定される終了位置
 │     │ └ "かつ丼定食"
 │     └ "だ。"

Node の一部のテキストだけを選択する

これまでの方法は、選択範囲の開始/終了 Nodeが div などの htmlタグを
指定していました。
この方法の場合、タグの中の全てのテキストを選択出来ても、
タグ内の一部のテキストのみを選択させる事が出来ません。

部分選択を実現するには、タグ内の TextNode を対象に
setStart() setEnd()関数を呼ぶ必要があります。

[実行結果]
要素内のテキスト部分選択実行結果

[JS]

// ~~ 省略 ~~

// 選択範囲開始を、div#line2 に設定
let  elStart  =  document.getElementById("line2");
range.setStart(elStart, 0);  //①

// 選択範囲終了を、bigタグ内のテキストの3文字目までに設定
let  elEnd  =  document.querySelector("#line2 big").childNodes[0];
range.setEnd(elEnd, 3);    //②

// ~~ 省略 ~~

最後に

JavaScript で選択範囲の制御を行おうとすると、
Range クラスの仕組みや、要素ツリーの Offset 値を理解する必要があり、
初めはややこしいですが、慣れるとかなり柔軟な選択範囲の制御が
出来て、さまざまなアプリに応用できると思います。



その他参考情報

MDN Web Docs では、RangeクラスのsetStart() setEnd()関数は、
以下のように紹介されています。

Range.setStart ( startNode, startOffset )

startNode が Text, Comment, あるいは CDATASection タイプの Node であるとき、
startOffsetはstartNodeの開始位置からの文字数です。
その他のNodeタイプの場合、 startOffsetはstartNodeからの子ノード数です。

引数 説明
startNode Range を開始する Node
startOffset Rangeの開始位置を示すstartNodeオフセット(非負整数)

Range.setEnd ( startNode, startOffset )

endNode が Text, Comment, あるいは CDATASection タイプの Node であるとき、
endOffsetはendNodeの開始位置からの文字数です。
その他のNodeタイプの場合、 endOffsetはendNodeからの子ノード数です。

始点よりも上の(文書の上位)終点を設定すると、
始点と終点が両方とも指定された終点に設定された折りたたまれた範囲になります。

引数 説明
endNode Range を終了する Node
endOffset Rangeの終了位置を示すendNodeオフセット(非負整数)
スポンサーリンク

QooQ