テキストエリアの選択範囲を正しく取得する方法

入力フォームのテキストエリアに、ブロックインデントなどの機能を追加するためには、現在の選択範囲を正しく取得する必要があります。selectionStart, selectionEndプロパティがサポートされていれば話は簡単なのですが、IEはサポートされていません。IEではTextRangeオブジェクトを使うのですが、このtextプロパティは選択範囲末尾の改行を削除してしまうという変わった仕様のため、単純なやり方だと、1行目の行末と2行目の行頭を区別することができません。これらを考慮して、正しい選択範囲をとれるコードを書きました。
処理の流れは次の通りです。

  1. selectionStart プロパティがあれば selectionStart と selectionEnd を返す
  2. 選択範囲のTextRangeオブジェクトを取得する(selectedRange)
  3. 選択範囲より前の部分のTextRangeを取得する(beforeRange)
  4. 上記2つのTextRangeについて、1文字ずつ範囲終了位置を狭めていき(moveEnd('character', -1))、選択文字数が0でない(compareEndPoints('StartToEnd', beforeRange) != 0)間、狭める前と後の文字列が変化する(beforeText0 != beforeRange.text)まで改行をつけたしていき、改行が削除される前の文字列(beforeText, selectedText)を復元する
  5. 復元した文字列の長さから選択範囲を計算する

IE8.0.6001.18865, Chrome4.0.249.78, Firefox3.6, Opera10.10, Safari4.0.4で動作することを確認しました。

http://3qi.net/selection.html
で動作を確認できます。

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
<title>テキストエリアの選択範囲を取得する</title>
<script type="text/javascript">

/* 選択範囲を取得 */
function getSelection( /* 選択範囲{ start : ?, end : ? } */
input                  /* 対象とするinputまたはtextarea */
) {
  if(input.selectionStart != undefined) {
    return { start : input.selectionStart, end : input.selectionEnd }
  }

  input.focus()
  var selectedRange = document.selection.createRange()
  var beforeRange = document.body.createTextRange()
  beforeRange.moveToElementText(input)
  beforeRange.setEndPoint('EndToStart', selectedRange)
  
  var beforeText0 = beforeRange.text
  var beforeText = beforeText0
  while(beforeRange.compareEndPoints('StartToEnd', beforeRange) != 0) {
    beforeRange.moveEnd('character', -1)
    if(beforeText0 != beforeRange.text)break
    beforeText += '\r\n'
  }

  var selectedText0 = selectedRange.text
  var selectedText = selectedText0
  while(selectedRange.compareEndPoints('StartToEnd', selectedRange) != 0) {
    selectedRange.moveEnd('character', -1)
    if(selectedText0 != selectedRange.text)break
    selectedText += '\r\n'
  }

  var beforeLength = beforeText.length
  return { start : beforeLength, end : beforeLength + selectedText.length }
}

window.onload = function() {

  var input = document.getElementById('input')
  var info = document.getElementById('info')
  var requireUpdate = false
  
  var update = function() {
    setTimeout(function() {
      var selection = getSelection(input)
      info.innerHTML = '(' + selection.start + ':' + selection.end + ')'
        + (selection.end - selection.start) + '文字選択中<br/>'
        + input.value.substring(selection.start, selection.end).replace(/\r\n|\n/g, '<br/>')
    }, 0)
    requireUpdate = false
  }
  
  input.onkeydown = function(e) {
    requireUpdate = true
  }
  
  input.onkeypress = function(e) {
    update(this)
  }
  
  input.onkeyup = function(e) {
    if(requireUpdate)update(this)
  }
  
  input.onmouseup = function() {
    update(this)
  }
  
  update(this)
  input.focus()
}
</script>
<form>
<textarea id="input" cols="80" rows="10"></textarea><br/>
<span id="info" style="background-color:#ffffcc"></span>
</form>