Home>Programming>プログラミングチップス>JavaScript/DOM>document>テキストエリアの選択範囲の位置を特定する

テキストエリアの選択範囲の位置を特定する

2006/6/12/1/12/6/17/
テキストエリアの選択範囲の位置を取得してあれしてあーしたい時があります。
しかし、IEにはNNのselectionStartプロパティ、selectionEndプロパティのような直接位置を取得するようなメソッドやプロパティはありません。NNでの取得方法は、テキストエリアの選択範囲を取得するに載っています。
そこで自前で取得するしかないのがMicrosoftの陰謀です。
NNの場合は説明するまでもないので、ここから先の話しはIE限定です。

まず、位置といってもその用途によって以下の2通りがあります。

  • NNのように位置情報をsubstringメソッド等を使って処理を共通化させたい場合
  • IEのTextRangeオブジェクトのmoveStartメソッド、moveEndメソッドを使いたい場合

1番目、2番目とも求め方は変わらないのでほとんど共通ですが、まず1番目から説明します。


その前に、IEとNNでは選択範囲の実装の違いから共通化できない部分があります。
それは、IEのTextRangeオブジェクトで取得した範囲にもしラストに改行コード¥r¥nがあった場合それが含まれないのです。(視覚的な選択範囲には含まれています)
NNでは選択していないように見えますがちゃんと改行コードは含まれていてます。
実際にどういったことになるかというと以下のようになります。





青色の部分が視覚的に選択されている部分で、灰色の部分が内部で選択されている部分です。
赤いテキストが、実際にデータとして選択されているものです。
選択範囲のラストに改行コードがあっても実際のデータに影響がないのはIEの仕様ですのであきらめるしかありません。


位置の特定方法の大まかな流れとしては、テキストエリア全体の文字列の長さから選択範囲の始点からテキストエリアの終端までの文字列の長さを引くという計算式で求められます。
選択範囲の始点からテキストエリアの終端までの文字列を取得するには、現在の選択範囲をテキストエリアの終端まで移動させて取得します。
こんな回りくどいことをしないとできないのには理由があります。
テキストエリアの始点から選択範囲の始点までの長さを使った場合、もし選択範囲の始点よりも前に改行コードがあった場合TextRangeオブジェクトの仕様から改行コードが含まれないので正確には求められないからです。

さて、実際のスクリプトの処理手順はまず、

  • A. テキストエリア全体の長さを取得する
  • B. 選択範囲の始点からテキストエリアの終点までの選択範囲を作って長さを取得する
  • C. AからBを引いて位置を特定する

まずAですが、ElementオブジェクトのcreateTextRangeメソッドを使います。(valueプロパティではラストに改行コードがあった場合含まれてしまい、Bの時にラストの改行コードは絶対に含まれないので計算上食い違いが発生するので使えません)
しかしこのメソッドは全てのElementオブジェクトにあるわけではありません。
このメソッドが使用できるElementオブジェクトは以下のように決まっています。

  • BODY
  • BUTTON
  • INPUT type=button
  • INPUT type=hidden
  • INPUT type=password
  • INPUT type=reset
  • INPUT type=submit
  • INPUT type=text
  • TEXTAREA

このメソッドはElementのテキストを含んだTextRangeオブジェクトを返します。 書式は次の通りです。

Element .createTextRange メソッド

書式
TextRange createTextRange( Void );
引数
  • なし
戻り値
TextRangeオブジェクトが返る。
特にcreateRangeメソッドと変わりませんがデータの範囲が違うだけです。
このメソッドを使えば、テキストエリア全体の範囲のテキストデータが取得できるというわけです。

次にBは、TextRangeオブジェクトのmoveEndメソッドを使って選択範囲の終点をオリジナルの終端に移動・・・したいところなのですが、Textareaエレメント上のselectionオブジェクトから作成したTextRandeオブジェクトでは使えません。
このメソッドの書式は次のようになっています。

TextRange .moveEnd メソッド

書式
Integer moveEnd(
	String sUnit
	[, Integer iCount]
);
引数
  • sUnit - 移動する単位数を、
    "character"(文字単位)、
    "word"(単語単位)、
    "sentence"(文単位)、
    "textedit"(オリジナル範囲の最初か最後)の文字列で指定。
  • iCount - 移動する単位数を整数で指定。正の場合は進んで、負の場合は戻る。デフォルトは1。
戻り値
移動した単位数が整数で返る。
TextRangeオブジェクトのmoveStartメソッドもありますがこれは範囲の始点というだけであって基本的に上の書式と同じです。
で、普通なら第1引数に"textedit"とすればオリジナルの終点に移動するわけなんですけど、Textareaエレメント上のselectionオブジェクトから作成したTextRangeオブジェクトではBODY要素がオリジナルということになっており、明後日の方向へ飛んでいきます。
つまり使えません。

次によく使われている方法です。
TextRangeオブジェクトにはoffsetLeftプロパティとoffsetTopプロパティという選択範囲の始点の座標を取得できるプロパティがあります。
このプロパティはウィンドウを基準とした座標です。
ElementオブジェクトにあるoffsetLeftプロパティ等と変わりません。

この座標を使って新しく選択範囲を作成することができるmoveToPointメソッドというのがあります。
これらを使ってスクリプトを組むと以下のようになります。
// 選択範囲の始点からテキストエリアの終点までの選択範囲を作成する

// ※eはTextareaのエレメント

e.focus();

// フォーカスのあたっている選択範囲を取得
var r = document.selection.createRange();

// エレメントのテキスト範囲を取得
var er = e.createTextRange();

// 選択範囲の座標を使ってエレメントのテキスト範囲を移動させる
er.moveToPoint( r.offsetLeft, r.offsetTop );

// 上の処理によって終点が移動してしまったのでオリジナルの終点に移動させる
// erでないといけないのは先ほど説明したとおりです。
er.moveEnd( "textedit" );
上記のスクリプトは、特定の場合を除いて上手く動作してくれます。
その特定の場合というのは、選択範囲がテキストエリアからはみ出てしまった場合です。
Textareaエレメントから作ったTextRangeオブジェクトは、moveToPointメソッドで指定できる範囲が以下のように決まっています。



上の例ではoffsetTopがテキストエリアよりも上の座標を示していて、moveToPointを使っても移動できないのです。
エラーも何も出ないのでそのまま使っても、テキストエリアの始点からの座標になっており位置の特定ができてません。
あまり状況的に気づきにくいようで、そのまま使っている人をよく見かけます・・・。
ということでこれも使えません・・・(ToT)

これで最後になりますが正真正銘の本命です。
TextRangeオブジェクトのsetEndPointメソッドを使います。

TextRange .setEndPoint メソッド

書式
Void setEndPoint(
	String sType,
	TextRange oTextRange
);
引数
  • sType - 転送する位置を、
    "StartToEnd"(oTextRangeの始点→thisの終点)、
    "StartToStart"(oTextRangeの始点→thisの始点)、
    "EndToStart"(oTextRangeの終点→thisの始点)、
    "EndToEnd"(oTextRangeの終点→thisの終点)の文字列で指定。
  • oTextRange - 適用するTextRangeオブジェクトを指定。
戻り値
なし。
つまり、先ほどのスクリプトに当てはめて言うと『er.moveToPoint( r.offsetLeft, r.offsetTop );』の部分は選択範囲の始点を新しい始点にしているわけですから、"StartToStart"を使えばうまくいきますね。
しかし・・・まだあるんです・・・実はこのメソッドはTextareaからcreateTextRangeメソッドで作成したTextRangeオブジェクトでは使えません!(汗)
なんなんでしょうかと、MSに殴り込みたいのですが仕様なので仕方ないのでしょう・・・。
コレを解決する策として、TextRangeオブジェクトのmoveToElementTextメソッドを使います。

TextRange .moveToElementText メソッド

書式
moveToElementText( Element oElement )
引数
  • oElement - 移動先のエレメントオブジェクトを指定。
戻り値
なし。
これは現在の範囲からエレメントのテキスト範囲に移動するというメソッドです。
つまり、createTextRangeメソッドを持たないElementオブジェクトのためにこのメソッドはあると言っても過言ではありません。
このメソッドを使って選択範囲の位置を特定するスクリプトを組むとこのようになります。
// 選択範囲の位置を特定する関数(IE専用)
function getSelectionPos_IE(e)
{
	e.focus();
	
	var r = document.selection.createRange();

	// 選択範囲のテキストの長さを取得する
	var len = r.text.length;

	// BODY要素のテキスト範囲を作成する
	var br = document.body.createTextRange();

	// BODY要素のテキスト範囲をeのテキスト範囲に移動する
	// これはe.createTextRange()とほぼ同等
	br.moveToElementText( e );

	// eのテキストの長さを取得する
	var all_len = br.text.length;

	// eのテキスト範囲の始点を、rの範囲の始点に移動する
	br.setEndPoint( "StartToStart", r );

	// 始点 = 全体 − (rの始点からeの終点) 
	var s = all_len - br.text.length;

	// 終点 = 始点 + 選択範囲の長さ
	var e = s + len;

	// 始点と終点を含むオブジェクトを返す
	return { start: s, end: e };
}

// クロスブラウザを考慮すると
function Foo(e, pullout)
{
	if( document.selection ) {

		var pos = getSelectionPos_IE(e);

	} else if( e.setSelectionRange ) {
		var pos = { start: e.selectionStart, end: e.selectionEnd };
	}

	if( pullout ) {
		alert( e.value.substring(pos.start, pos.end) );
	} else {
		alert( "[start="+pos.start+", end="+pos.end+"]" );
	}
}
IEとNNの両方で実行してもらえればわかりますが取得した位置の数値が多少違ってきます。
これはIEでは改行コードが¥r¥nに対し、NNでは改行コードが¥nのみだからです。
ブラウザによる改行コードの違いだけなので選択した文字列は正しく抜き出せているのです。
ですのでその辺は基本的に気にしなくてもいいのです。


長くなってしまいましたが2番目の方法の、TextRangeオブジェクトのmoveStartメソッドとmoveEndメソッドで使う場合です。
substringメソッドの位置を指定する引数が文字列の長さ=位置(正確には1つづれる0からの値で、¥r¥nもそれぞれ1文字として扱っている)というのに対して、TextRangeオブジェクトのmoveStartメソッド、moveEndメソッドの場合は¥r¥nを1文字(1ポジション)として扱うため位置を特定する段階でこれを2文字から1文字とする処理がいります。
難しい処理がいりそうだと思いますが、上のスクリプトを3行書き換えるだけですみます。
// 選択範囲の位置を特定する関数2(IE専用)
function getSelectionPos2_IE(e)
{
	e.focus();
	
	var r = document.selection.createRange();

	var len = r.text.replace(/\r/g, "").length;

	// BODY要素のテキスト範囲を作成する
	var br = document.body.createTextRange();

	// BODY要素のテキスト範囲をeのテキスト範囲に移動する
	// これはe.createTextRange()とほぼ同等
	br.moveToElementText( e );

	// eのテキストの長さを取得する
	var all_len = br.text.replace(/\r/g, "").length;

	// eのテキスト範囲の始点を、rの範囲の始点に移動する
	br.setEndPoint( "StartToStart", r );

	// 始点 = 全体 − (rの始点からeの終点) 
	var s = all_len - br.text.replace(/\r/g, "").length;

	// 終点 = 始点 + 選択範囲の長さ
	var e = s + len;

	// 始点と終点を含むオブジェクトを返す
	return { start: s, end: e };
}

function Baz(e)
{
	if( document.selection ) {

		var pos = getSelectionPos2_IE(e);
		
		// 始点と終点がmoveStart、moveEndで使えるか検査

		var r = e.createTextRange();
		
		// moveEndメソッドのカウント値の扱いをテキストの長さとして使うため
		r.collapse();

		// 選択範囲を作成する
		r.moveStart( "character", pos.start );
		r.moveEnd( "character", pos.end-pos.start );

		alert( r.text );
	}

}
このようにmoveStartメソッドとmoveEndメソッドを使う場合は少し注意しなくてはなりません。

今までの注意点のまとめ。

  • TextRangeオブジェクトは範囲の終端には改行コードを含められない。
  • テキストエリア上のselectionから作成したTextRangeオブジェクトはBODY要素がオリジナル。
  • moveToPointは選択範囲がはみ出た位置(座標)は使用できない。
  • setEndPointはTextareaのTextRangeオブジェクトは使用できない。
  • moveStart、moveEndでのポジションの指定で¥r¥nは1文字として数える。
  • IEのテキストエリアの改行コードは¥r¥n、NNでは¥nのみ。
(作成日:2006.05.21)