選択文字列部分のタグのid取得について

[新着] Webテンプレートを仮オープンしました



0   名前: shuu : 2006/10/11(水) 22:50  ID:nv3uUquy
選択文字列をgetRangeAtを使ってspanで囲むようにしたのですが、
その後に、簡単そうな部分がわかりません。

ABCDE<span id="id1" 〜>FGHIJ</span>
KLMNO<span id="id2" 〜>PQRST</span>UVWXYZ

上の状態はA〜ZでFGHIJとPQRSTを選択して
それぞれgetRangeAtでspanを付けた後なんですが、
ここで、PQRSTやQRSをgetRangeAtで選択したとき、
どうすれば、id属性の「id2」を取り出せるのかがわかりません。

JavaScriptやDOMに関しては初心者で、初歩的なことなのかも
しれませんが、どなたか教えてください。

1   名前: 匿名 : 2006/10/11(水) 22:50  ID:i4s77xzC
そこに至る過程でspanオブジェクトを作成しているはずなのでidプロパティを見ればいいだけのような気もするし、
Rangeのオフセット位置によってはcommonAncestorContainerを見るのもありかもしれないし、

そこまで行く過程で何をしたか不明なので何とも返答に困る。

2   名前: shuu : 2006/10/11(水) 22:50  ID:FeV4lfUa
返信ありがとうございます。
はじめてこのように質問するもので、大雑把な説明で申し訳ありません。

選択文字列に色を付ける際には、Web上で調べたことより、

var range = window.getSelection().getRangeAt(0);
var marker = document.createElement('span');
marker.setAttribute('style', 'background: cyan');
marker.setAttribute('id', 'TEST_1');
range.surroundContents(marker);

上記表現でできることを知り、使用しました。
そして、色をつけていくごとにidをTEST_2,3と変えていっています。
そこである部分のハイライトの全体か、一部を消そうと思ったときに、
その部分のidを知る必要が出てきたのですが、それがわかりませんでした。

こんな感じでわかりますでしょうか。

3   名前: 匿名 : 2006/10/11(水) 22:50  ID:i4s77xzC
getElementByIdではダメなの?

もう少し操作の具体的なイメージを書いてくれるとありがたいんだけど。
あと、失敗でもいいから前後のソースコード出してくれるとなお助かる。

4   名前: shuu : 2006/10/11(水) 22:50  ID:nv3uUquy
一般で言うマーカー機能で、全体は文字列を選択してbuttonを押すことで
そこに線を引く、または消すといったものです。

で、下がマーカーを引く部分でして、

var range = window.getSelection().getRangeAt(0);
var marker = document.createElement('span');
marker.setAttribute('id', 'SPA_0');
range.surroundContents(marker);
WORD[count]=range;  //WORD[]にはこれまでマーカーを引いた文字列を入れています。
document.FORM1.SELE2.length++;
document.FORM1.SELE2.options[count].text=range;  //それらをセレクトメニューに追加
$("SPA_0").id="SPA_"+count;
spansID=$("SPA_"+count).id;
$(spansID).style.backgroundColor=COL;  //COLはあらかじめ設定した色です。
count++;

無駄が多そうですが、セレクトメニューに選択文字列が表示しており、
マーカーを引くたびにそのspanのid名の数が増えていくようにしています。

そして、今困っているのが、消すときなんですが、マーカー部分を
選択して消去ボタンを押すとき、そのマーカーが何番目に引いたものかを
idを見ることで調べたいのですが、そのidを取り出せません。
選択文字列だけから取り出すので、getElementById(" ")の""に
入るものもわからないんです。長々とすみません。

5   名前: 匿名 : 2006/10/11(水) 22:50  ID:i4s77xzC
>>4
> 全体は文字列を選択してbuttonを押すことでそこに線を引く、または消す

ボタンを押した瞬間にフォーカスが取られて選択範囲が解かれると思うんだが、今は置いとく。
XULアプリかbookmarkletなのかもしれないし。
ただ、prototype.jsのような標準でないライブラリを使っているならば一言断ろう。

> マーカー部分を選択して消去ボタンを押す

それは分かったけど、マーカー部分を完全に選択するの?
それとも一部かかってればいいの?
もし二個以上のマーカーに引っかかったらどうするの?
それによって難易度が全然違う気がするから「具体的なイメージ」と言ったんだけどね。

一番簡単なのはクリックでマーカーを消すこと。span要素にclickリスナを仕込むだけ。

完全に選択するなら、それぞれのRangeのstartOffsetとendOffsetを比較するのが手っ取り早いだろう。

部分的に引っかかってもいい場合はRange::compareBoundaryPointsを使う。
やり方はhttp://piro.sakura.ne.jp/xul/tips/x0031.htmlに載っている。
ややこしそうに見えるが、実際にやってみればそれほど難しくはない。

二個以上のマーカーにかかってもいい場合、全てのマーカーに対してcompareBoundaryPointsでチェックする。
ただ、FirefoxならばDOM3 Coreのdocument.compareDocumentPositionを使えるから、
あらかじめWORD内のRangeを文書出現順にソートしておけば、無駄な処理をせずに済むかもしれない。

マーカ用Rangeからは、range.startContainer.childNodes[range.startOffset]でspan要素が見えるはず。
もっとも、Rangeさえ同定できればマーカは消せるから、処理中にこれが必要になることはまずないだろう。
だから、マーカを消すためにid属性を付けるのは無意味なんだが、
他の用途で使うことになるかもしれないから、付けたままでもいいかもしれない。

具体的なサンプルを書こうかと思ったんだが、
最初に述べたように「ボタンを押すってありえなくないか?」でまずひっかかってしまったし、
そこでゴチャゴチャするのも嫌なので説明だけにした。ご容赦。

6   名前: 匿名 : 2006/10/11(水) 22:50  ID:i4s77xzC
>>5
すまん、難しく考えすぎていた。俺が誤読というか質問の意図を読み取れてなかった。
要するに、削除範囲の中に、span[@id="SPA_n"]要素が含まれているかどうか調べることができればいいんだね。

削除範囲の、startContainerからendContainerまで要素ノードを再帰的に検索すればいい。
ノード検索関数を自作してもいいが、こういう用途ならTreeWalkerを使わない手はない。
(本当はNodeIteratorの方がシンプルでいいんだが、実装は皆無)

// 削除範囲
var range = window.getSelection().getRangeAt(0);

// 削除範囲の中身を検索するTreeWalker
var walker = document.createTreeWalker(
    range.commonAncestorContainer,  // 検索対象とする部分ノードの頂点
    NodeFilter.SHOW_ELEMENT,        // 要素ノードだけを検索する
    function (n) {                  // 要素ノードの条件フィルタ
        if (n.tagName == 'SPAN' && n.id.match(/^SPA_/)) {
            // span要素かつid="SPA_n"ならばそのノードを受け入れる
            return NodeFilter.FILTER_ACCEPT;
        } else {
            // それ以外の要素ノードはスルーする
            return NodeFilter.FILTER_SKIP;
        }
    },
    false                           // 実体参照を展開しない
);

// 削除範囲の開始位置を含むノードから開始
walker.currentNode = range.startContainer;

while (walker.nextNode()) {
    // 削除用Range内に含まれるspan[@id="SPA_n"]要素が
    // walker.currentNodeとして順番に返ってくる。
    
    // 削除範囲の終了位置を含むノードに達したら終了
    if (walker.currentNode == range.endContainer)
        break;
}


まぁ、もう少し効率的に組めるだろうから考えてみて(笑
ちなみに、フィルタでreturnするまえにフックして一気に処理を行うこともできるが、
検索中にノードツリーを変化させるとややこしいので止めた方が無難だろう。

7   名前: shuu : 2006/10/11(水) 22:50  ID:FeV4lfUa
返事が遅くなってすみません。

とりあえず>>5の返信として
prototype.jsは使ってます、これが何するかよくわかってませんが、
getElementByIdが$に省略するだけな感じでしか使ってなかったので言い忘れていました。
とりあえずフォーカス移動は起きませんでした。

削除方法は、できれば一部分や全体などどれも対応させたいです。

それ以降のことは、なにしろDOM2,3がよくわからず、参照リンクの方のページも
よく利用させてもらっていますが、まず勉強不足みたいなのでそこをとことん学ぼうと思います。

>>6の返信
>誤読というか質問の意図を読み取れてなかった。
いえ、はっきり言って自分の説明不足と説明下手さによるものです。

で、サンプルを書いていただきありがとうございます。
TreeWalkerははじめて知りました。参考にさせてもらい頑張ってみます。

8   名前: 匿名 : 2006/10/11(水) 22:50  ID:i4s77xzC
> とりあえずフォーカス移動は起きませんでした。

本当?じゃあ俺の環境だけなのかな。
Firefox 1.5/Win、Mozilla 1.8/Macのどちらもフォーカス取られちゃうんだよね。
とにかく、完成したらソースコードを見せてほしい、と言ってみる(笑

> なにしろDOM2,3がよくわからず

情けないことにIEが対応してない分、日本語の情報がほとんど無いしね。
DOM2 Traversal and Rangeの基本的な考え方を身に着けるには、はっきり言って仕様書を読むしかない。
http://www2u.biglobe.ne.jp/%7Eoz-07ams/prog/dom-ref/Traversal/index.html
http://www2u.biglobe.ne.jp/%7Eoz-07ams/prog/dom-ref/Ranges/index.html

マーカー用スクリプトを作る以上、これらを読破するのは避けられない道だと思う。
実は>>5-6の説明も、基本モデルを理解していることを前提に書いている。

ただ、Range ModelやNodeIterator/TreeWalkerはそれほど難しいものじゃない。
慣れてしまえば、Coreメソッドだけでちまちまやるよりずっと楽。個人的にはかなりお勧め。
(もっと言えばFirefoxならDOM3 XPathもある程度使えるが、まだ実用的とは言えない)

> TreeWalkerははじめて知りました。参考にさせてもらい頑張ってみます。

今見返したら>>6にはバグがあるので、>>6を参考にして何か変なことになったら知らせて。
そのバグに対してだけは責任とるから(苦笑

9   名前: shuu : 2006/10/11(水) 22:50  ID:nv3uUquy
> とりあえずフォーカス移動は起きませんでした。
よくみたらbuttonの部分onmousedownで起動にしてました。
そのためだと思います。

> DOM2 Traversal and Rangeの基本的な考え方を身に着けるには、はっきり言って仕様書を読むしかない。
やっぱりそうですか。仕様書以外にもっとわかりやすいものがないかと調べてみたのですがまったく見つからず困っていました。

>>6のバグですか?ちょっとまだよくわかってませんね。

実際に今現在のソースを全部見ていただければ、一番嬉しいのですがちょっと公には公開したくない(してはいけない?)感じなので、直接連絡していただければ幸いです。

10   名前: 匿名 : 2006/10/11(水) 22:50  ID:i4s77xzC
> よくみたらbuttonの部分onmousedownで起動にしてました。

あーそうか! ありがとです。

公開できる部分だけをまとめることはできない?
意地悪で言ってるんじゃなくて、せっかくこういう場なんだから大勢に見てもらうのが一番だろうし、
その方がいわゆる過去ログとして再利用されやすいし、
何より、まとめを行うことで自分自身の知識の整理にもつながる。
ごちゃごちゃしていたソースコードがいつの間にかすっきり、なんてことも(笑

DOM2 Rangeにおけるコンテナとオフセットの数え方とか、
DOM2 Traversalにおけるノードフィルタの書き方とか、
そもそもDOMにおけるノードって何だ?とか、
些細なことでも質問してくれればいろいろアドバイスをもらえるだろうし、俺もわかる範囲で答えるよ。
(ログの検索性を考えれば、一つの質問は一つのスレッドが望ましいだろうけど)

この分野は本当に資料が少ないので、どんな簡単なことでも、公開ログは貴重なんだ。

11   名前: shuu : 2006/10/11(水) 22:50  ID:nv3uUquy
えーと、まず長くてごめんなさい。
結局現状況全部のせました。
<HTML><HEAD><TITLE>Sample</TITLE>
<SCRIPT Language="JavaScript" src="prototype.js"></script>
<SCRIPT Language="JavaScript">
<!--
	var x=1;
	var WORD=new Array;
	WORD[0]="test_WORD";
function linemarker(){
	var COL=document.FORM1.SELE.value;
	b=$("DIV1");
	if(window.getSelection()!=""){
		if(COL=="clear"){
			var range_word = window.getSelection().getRangeAt(0);
			str=window.getSelection().toString();
			thisID=range_word.commonAncestorContainer.parentNode.id;
			var splitNo=thisID.split("_");
			var GetNo=Number(splitNo[1]);
//選択文字列完全一致削除
			if(str==WORD[GetNo]){
				var CLRWORD=WORD[GetNo];
				for(m=GetNo;m<x-1;m++){
					WORD[m]=WORD[m+1]; WORD[m+1]="";
					$("SPA_"+(m+1)).id="SPA_"+m;
					document.FORM1.SELE2.options[m].text = document.FORM1.SELE2.options[m+1].text;
				}
				x--;
				deleteNode = document.getElementById(thisID);
				range2 = document.createRange();
				range2.selectNodeContents(deleteNode);
				dummy = range2.extractContents();
				range2.setStartBefore(deleteNode);
				range2.insertNode(dummy);
				range2.selectNode(deleteNode);
				range2.deleteContents();
				document.FORM1.SELE2.length--;
			}
//選択文字列部分削除
			else {
				thisTag=range_word.commonAncestorContainer.parentNode.tagName;
				if(thisTag=="SPAN"){
					var dl_mark = document.createElement('span');
					dl_mark.setAttribute('id', 'SPA_0');
					range_word.surroundContents(dl_mark);
					var pId=$("SPA_0").parentNode.id;
					var pColor=$("SPA_0").parentNode.style.backgroundColor;
					var pNo=pId.split("_");
					allDoc=b.innerHTML;
					b.innerHTML=allDoc.replace('<span id="SPA_0">'+range_word+'</span>','</span>'+range_word+'<span id="SPA_0">');
					$("SPA_0").id="SPA_"+x;
					spansID=$("SPA_"+x).id;
					$(spansID).style.backgroundColor=pColor;
					if($(pId).innerHTML==""){						//選択文字列がspanの先頭の場合
						$(pId).removeAttribute("style");
						$(pId).removeAttribute("id");
						allDoc=b.innerHTML;
						b.innerHTML=allDoc.replace('<span>','');
						$("SPA_"+x).id=pId;
						WORD[pNo[1]]=$(pId).innerHTML;
						document.FORM1.SELE2.options[pNo[1]].text=WORD[pNo[1]];
						x--;
					}
					else if($("SPA_"+x).innerHTML==""){			//選択文字列がspanの最後尾の場合
						$("SPA_"+x).removeAttribute("style");
						$("SPA_"+x).removeAttribute("id");
						allDoc=b.innerHTML;
						b.innerHTML=allDoc.replace('<span>','');
						WORD[pNo[1]]=$(pId).innerHTML;
						document.FORM1.SELE2.options[pNo[1]].text=WORD[pNo[1]];
						x--;
					}
						else{
						WORD[pNo[1]]=$(pId).innerHTML;
						document.FORM1.SELE2.options[pNo[1]].text=WORD[pNo[1]];
						WORD[x]=$("SPA_"+x).innerHTML;
						document.FORM1.SELE2.length++;
						document.FORM1.SELE2.options[x].text=WORD[x];
					}
					x++
				}
				else{
				}
			}
		}
		else{
			var range = window.getSelection().getRangeAt(0);
			var marker = document.createElement('span');
			marker.setAttribute('id', 'SPA_0');
			range.surroundContents(marker);
			WORD[x]=range;
			document.FORM1.SELE2.length++;
			document.FORM1.SELE2.options[x].text=range;
			$("SPA_0").id="SPA_"+x;
			spansID=$("SPA_"+x).id;
			$(spansID).style.backgroundColor=COL;
			x++
		}
	}
}

function test_inner(){
	alert($("DIV1").innerHTML);
}

//-->
</SCRIPT>
</HEAD>
<BODY id="BODY1">
<CENTER>
<H1>選択された文字の取得</H1>
</CENTER>
<form name="FORM1">
<select name="SELE">
<option value="cyan">cyan</option>
<option value="red">red</option>
<option value="yellow">yellow</option>
<option value="clear">clear</option>
</select>
<INPUT Type="button" value="check" onMouseDown="linemarker()">
<select name="SELE2">
<option>ハイライト文字列</option>
</select>
<br>
<INPUT Type="button" value="inner" onMouseDown="test_inner()"><br>
</form>
<div id="DIV1">
テキストサンプル:文字列を選択してcheckボタンで色付けor削除。<br>
innerは指定ウィンドウの内容表示です。 <br>
セレクト「ハイライト文字列」にはマーク文字列が入ります。<br>
</div>
</BODY></HTML>

だらだらと無駄な部分が多そうな上、check時やマーク文字表示などまだまだバグが絶えませんが、そのへんは許してください。

12   名前: 匿名 : 2006/10/11(水) 22:50  ID:XlBZ2xrO
選択範囲が他のマーカーに部分的にでも重なっている場合:
・「ハイライトする」で選択範囲とマーカーを結合
・「ハイライト消去」でそのマーカーを消去
するようにしてみたけど、バグあるかも。

/*
document.implementation.hasFeature('HTML' , '2.0')
document.implementation.hasFeature('CSS2' , '2.0')
document.implementation.hasFeature('Range', '2.0')
Array.prototype.forEach
*/

var MarkerList = [ ];

function getSelectionToAppendMarker (formElement) {
	var selection = getSelection();
	if (selection.toString()) {
		var range = selection.getRangeAt(0);
		var element = document.createElement('span');
		selection.collapseToStart();
		element.style.backgroundColor = formElement.elements['select.color'].value;
		MarkerList = checkMarkedRange(MarkerList, range);
		MarkerList.unshift( { range : range, element : element } );
		range.surroundContents(element);
		displayStringList(formElement.elements['select.string'], MarkerList);
	}
}

function getSelectionToDeleteMarker (formElement) {
	var selection = getSelection();
	if (selection.toString()) {
		var range = selection.getRangeAt(0);
		selection.collapseToStart();
		MarkerList = checkMarkedRange(MarkerList, range);
		range.commonAncestorContainer.normalize();
		displayStringList(formElement.elements['select.string'], MarkerList);
	}
}

function displayStringList (selectElement, markerList) {
	deleteNodeContents(selectElement);
	markerList.forEach (
		function (marker) {
			var optionElement = document.createElement('option');
			optionElement.appendChild(document.createTextNode(marker.range.toString()));
			selectElement.add(optionElement, null);
		} );
}

function checkMarkedRange (oldMarkerList, range) {
	var newMarkerList = [ ], tempList = [ ];
	oldMarkerList.forEach(
		function (marker) {
			if (compareRangesBoundary(range, marker.range)) tempList.push(marker);
			else newMarkerList.push (marker);
		} );
	tempList.forEach(function (marker) { deleteMarker(marker); } );
	return newMarkerList;
}

function compareRangesBoundary (range1, range0) {
	if (range1.compareBoundaryPoints(Range.START_TO_END, range0) ==  1 &&
		range1.compareBoundaryPoints(Range.END_TO_START, range0) == -1) {
		if (range1.compareBoundaryPoints(Range.START_TO_START, range0) == 1)
			range1.setStart(range0.startContainer, range0.startOffset);
		if (range1.compareBoundaryPoints(Range.END_TO_END, range0) == -1 )
			range1.setEnd(range0.endContainer, range0.endOffset);
		return range0;
	else
		return null;
}

function deleteMarker (marker) {
	var documentFragment, range = marker.range, element = marker.element;
	range.selectNodeContents(element);
	documentFragment = range.extractContents();
	range.setStartBefore(element);
	range.insertNode(documentFragment);
	range.selectNode(element);
	range.deleteContents();
	range.detach();
}

function deleteNodeContents (node) {
	var range = document.createRange();
	range.selectNodeContents(node);
	range.deleteContents();
	range.detach();
}

</script>
<h1>選択された文字の取得</h1>
<form action="#">
<p>
<select name="select.color">
<option value="cyan">cyan</option>
<option value="red">red</option>
<option value="yellow">yellow</option>
<option value="clear">clear</option>
</select>
<input type="button" value="ハイライトする" onmousedown="getSelectionToAppendMarker(this.form)">
<input type="button" value="ハイライト消去" onmousedown="getSelectionToDeleteMarker(this.form)">
</p>
<p>
<select name="select.string">
<option>ハイライト文字列</option>
</select>
</p>
</form>
<p>
テキストサンプル:文字列を選択して
「ハイライトする」ボタンで色付け、
「ハイライト消去」で削除。
「ハイライト文字列」にはマーク文字列が入ります。
</p>

13   名前: 匿名 : 2006/10/11(水) 22:50  ID:XlBZ2xrO
>>12
上が抜けた。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>Sample</title>
<script type="text/javascript">


なお字数制限のため、省略可能なタグは極力省略した。
# 今見たらp要素とoption要素の終了タグも削れたなあ。

14   名前: 匿名 : 2006/10/11(水) 22:50  ID:XlBZ2xrO
>>12
さらにコピペミス。
compareRangesBoundary関数の最後、else を } else に(括弧の対応ミス)

基本的な考え方だけ書くと
1. マーカーの情報は、{ range : 選択範囲, element : マーカー用エレメント } というobjectを要素とする配列MarkerListで管理
2. 選択範囲を拾ったとき、それとMarkerListに保存された全てのマーカーとの位置関係をチェックする
3. 選択範囲とマーカーが重なっていた場合、マーカーを覆うように選択範囲の長さを変更する
4. 3を繰り返し、最終的に選択範囲の中に含まれたマーカーは全て消去する
5. 残ったマーカーでMarkerListを再構築する。

新たにマーカーを作成する場合は、
6. 選択範囲からマーカーを作成してMarkerListに保存する

そして最後に
7. MarkerListの情報を使って、フォーム内の「ハイライト文字列」を更新する

のような感じ。2と3を実現するときにRange::compareBoundaryPointsを使っている。

15   名前: shuu : 2006/10/11(水) 22:50  ID:FeV4lfUa
いやー、関数にまとまってすっきりして見事ですね。
機能としても、自分のあったバグなどがなくていいですね。

これを基に頑張ろうと思います・・・が、ちょっとばかりこの機能に取り組むのが先延ばしになってしまう状況になってしまったので、結果がすぐに報告できそうにありません。
せっかく時間をかけて作ってくださったソースは今後とも参考にしていきたいと思います。
とりあえず、今回の件はとても勉強になりました。どうもありがとうございました。

一覧へ戻る