LoginSignup
25
19

More than 3 years have passed since last update.

ContentEditable な caret の位置(座標)を取得する

Last updated at Posted at 2019-05-26

ブラウザで動くエディタを開発していると避けては通れない contenteditable な caret の位置(座標)。いろいろなアプローチがありますが、一部のケース(縦書き)でうまく動かないことがあったので回避策をメモしておきます。

ダミーの DOM を挿入する

わりと古典的でよく使われている方法です。caret のある位置にダミーの DOM を挿入して getBoundingClientRect で位置を取得するというもの。どんな場面でもほぼ確実に caret の位置が取得できます。ただ、大きな弱点があり DOM を挿入するので対象 DOM の TextNode が分割されてしまいます。contenteditable を使ってゴリゴリ開発をする場合、むやみに Node が変わるとつらすぎるので、あまり使うメリットはありません。

<p contenteditable="true">click me</p>
<script>
  document.onselectionchange = () => {
    const range = window.getSelection().getRangeAt(0)
    const anchor = document.createElement('span')
    anchor.innerHTML = '&#8203;'
    range.insertNode(anchor)
    const pos = anchor.getBoundingClientRect()
    anchor.parentElement.removeChild(anchor)
    console.log(pos)
  }
</script>

Range オブジェクトの座標を取得する

caret 位置は Range オブジェクトで知ることができます。つまり単純に Range にたいして getBoundingClientRect を取得すれば caret の位置は取得できちゃいます。非常に簡単。通常はこちらを利用するのが良いと思います。

<p contenteditable="true">click me</p>
<script>
  document.onselectionchange = () => {
    const pos = window.getSelection().getRangeAt(0).getBoundingClientRect()
    console.log(pos)
  }
</script>

writing-mode: vertical-lr では機能しない

しかし、冒頭に書いた一部のケースでうまく動かないというのが縦書きです。CSS で writing-mode: vertical-lr にしている要素に対しては Chrome / Safari では上記の Range オブジェクトで getBoundingClientRect が正常に取得できません。すべての値が 0 で返ってきます。Firefox は正常に返ってくるので…バグのような気がしなくもないです。

<style>
  p{ writing-mode: vertical-lr; }
</style>
<p contenteditable="true">click me</p>
<script>
  document.onselectionchange = () => {
    const pos = window.getSelection().getRangeAt(0).getBoundingClientRect()
    console.log(pos) // すべて 0 になる…
  }
</script>

そこで回避策として以下のように Range オブジェクトを clone して選択範囲を指定し直すと、なぜか取得できるようになります。こちらでは正常に取得できる理由は分かりませんが…とりあえずこれで縦書きでも caret 位置を取得することができるようになりました。

<style>
  p{ writing-mode: vertical-lr; }
</style>
<p contenteditable="true">click me</p>
<script>
  document.onselectionchange = () => {
    const range = window.getSelection().getRangeAt(0)
    const clone = range.cloneRange()
    const fixedPosition = range.endOffset
    // 末尾の文字列を選択した時はダミーテキストを追加して選択範囲を拡大する
    if (fixedPosition + 1 > range.endContainer.length) {
      const dummy = document.createTextNode('&#8203;')
      clone.insertNode(dummy)
      clone.selectNode(dummy)
      const rect = clone.getBoundingClientRect();
      console.log(rect);
      dummy.parentNode.removeChild(dummy)
    } else {
      clone.setStart(range.endContainer, fixedPosition);
      clone.setEnd(range.endContainer, fixedPosition + 1);
      const rect = clone.getBoundingClientRect();
      console.log(rect);
    }
    clone.detach()
  }
</script>
25
19
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
19