ラインマーカーは、選択範囲の文字列をマーカーで強調したようにする拡張機能です。この機能を実装するためには、DOM2 Rangeを使用する必要があります。
GeckoやIEでは、window.getSelection()
によって選択範囲を取得することができます。しかしながら、これと既存のテクニック(document.write()
やHTMLElement.innerHTML
などを使った文字列的な処理)の組み合わせでラインマーカーと同様の処理を実現しようとすると、大変面倒なことになります。
以下に、起こりうる問題を列挙しました。
ラインマーカーでは、選択範囲を位置情報込みで扱い、且つDOM2 Rangeを使用することで、この問題を回避しています。というよりも、このような処理を実現するためには現在の所、以下に解説する方法を使うのが最も簡単だと思われます。参考文献一覧で紹介しているドキュメントを、まずは参照することをお勧めします。
なお、以下の例は全てGecko(Mozilla)でのものです。
contextMenuOverlay.jsで定義しているsetMarker
というメソッドが、この処理を担当しています。順を追って見ていきましょう。
選択範囲の情報は、windoe.getSelection()
で取得できます。このメソッドは選択範囲の文字列を取得するもの認識している人もいるかも知れませんが、それは実はこのメソッドの提供する機能の一つに過ぎません。
このメソッドの返り値はnsISelectionという型のオブジェクトで、選択範囲の文字列はこのオブジェクトのtoString()
メソッドで取得できます(このオブジェクトを文字列値を要求するコンテキストで参照した場合、toString()
メソッドが暗黙的に呼ばれるため、一見するとwindoe.getSelection()
は選択範囲の文字列を返すだけのメソッドと見えてしまう)。
このオブジェクトのメソッドのうち、今回重要なのはgetRangeAt()
メソッドです。このメソッドによって、選択範囲に相当するDOM2 Rangeのオブジェクトを取得することができます。
var range = window.getSelection().getRangeAt(0);
getRangeAt()
メソッドの引数には、何番目の選択範囲を取得するかを数値で指定します。これは、Ctrl-クリックによって表のセルを複数選択できるというGeckoの仕様に基づく機能です。通常の文字列選択では0番目、つまり最初の選択範囲が処理対象となります。
次に、選択範囲をマーカー用の要素で囲う処理ですが、これは2つのパターンがあります。以下、「[>]~[<]」は選択範囲を意味します。
<p>文字列[>]文字列[<]文字列</p>
<p>文字列文字列[>]文字列</p>
<p>テキスト[<]テキストテキスト</p>
ラインマーカーでは、それぞれの場合で異なる処理を行っています。
選択範囲が要素間を跨いでいない場合は、DOM2 RangeのsurroundContents()
メソッドが使えます。
var range = window.getSelection().getRangeAt(0);
var marker = document.createElement('span');
marker.setAttribute('style', 'background: red');
range.surroundContents(marker);
このようにすることで、以下のような処理結果が得られます。
<p>文字列<span style="...">文字列</span>文字列</p>
要素間を跨いだ選択範囲では、パターン1の処理ではうまく動きません。そのため、選択範囲に含まれる複数のテキストノードそれぞれをマーカー用の要素でラッピングするようにしています。これは先のソースコードでは、mWrapUpTextsというメソッドで処理を定義しています。
まず、選択範囲の前後でテキストノードを切り分けます。
var tempRange = targetWindow.document.createRange();
// 選択範囲の始点でノードを切り分ける
tempRange.setStart(range.startContainer, range.startOffset);
tempRange.setEndAfter(range.startContainer);
// <p>文字列文字列[tempRange→]文字列[←tempRange]</p>
tempRange.insertNode(tempRange.extractContents());
// <p>文字列文字列[ここで分割された]文字列</p>
// 選択範囲の終点でノードを切り分ける
tempRange.setEnd(range.endContainer, range.endOffset);
tempRange.setStartBefore(range.endContainer);
// <p>[tempRange→]テキスト[←tempRange]テキストテキスト</p>
tempRange.insertNode(tempRange.extractContents());
// <p>テキスト[ここで分割された]テキストテキスト</p>
tempRange.detach();
次に、選択範囲の中に含まれる最初のノードからスタートして、選択範囲の最後のノードに達するまで、全てのノードを一つずつチェックしています。その際、テキストノードに遭遇したときはそれをマーカー用の要素でラッピングしていきます。
ソースコードはここでは省略しますが、具体的には、前述のような例では以下のような処理が行われます。以下、「[≫]~[≪]」は処理中のノードを示します。
<p>文字列文字列[>][≫]文字列[≪]</p>
<p>テキスト[<]テキストテキスト</p>
<p>文字列[>]文字列[≫]<span style="...">文字列</span>[≪]</p>
<p>テキスト[<]テキストテキスト</p>
<p>文字列[>]文字列<span style="...">文字列</span></p>
[≫]<p>テキスト[<]テキストテキスト</p>[≪]
<p>文字列[>]文字列<span style="...">文字列</span></p>
<p>[≫]テキスト[≪][<]テキストテキスト</p>
<p>文字列[>]文字列<span style="...">文字列</span></p>
<p>[≫]<span style="...">テキスト</span>[≪][<]テキストテキスト</p>