uhyohyo.net

JavaScript初級者から中級者になろう

八章第三回 Rangeの活用とSelection

Selectionとは

前回でRangeの機能はだいたい解説しました。しかし、正直なところ、これをいつ使う機会があるのか分からないという人もいるかもしれません。しかし、Selectionというものを使うとき、Rangeが大きく関わってきます。

それでは、このSelectionとは何なのでしょうか。

これは、ユーザーがドラッグして選択した範囲を表すものです。始めから終わりまでマウスでドラッグして選択するわけですから、「開始点」と「終了点」がありますね。ここでRangeが登場するわけです。

Selectionの取得

Selectionは、Rangeとは違うオブジェクトです。Selectionを取得するには、getSelectionという関数を使います。引数はありません。

var selection = getSelection();

何かのメソッドでない関数というのも久しぶりですね。

これでSelectionオブジェクトを取得できました。しかし、なぜ直接選択範囲のRangeを取得できないか疑問に思うかもしれません。これにはいくつか理由がありますが、「選択していない場合」や「2つ以上選択している場合」がありえるというのが理由の1つです。

それでは、このSelectionオブジェクトからRangeを取得するには、メソッドgetRangeAtを使います。これは数値の引数を1つ持ち、何番目のRangeを取得するかを指定します。

基本は、1つしかRangeがないので0ですが、もし2つ以上の範囲を同時に選択している場合に1以上を使用することがあるかもしれません。ちなみに、今何個Rangeがあるかは、rangeCountというプロパティでわかります。Rangeを処理するときは、これが0かどうかで処理を分ける必要がでてきます。0の場合、そもそも選択していないのでRangeを取得できません。

Selectionの機能

よく使う機能は、SelectionからRangeを除去する機能です。選択部分をSelectionから除去すると、実際に選択が解除されます。

Selectionのメソッドで、removeRangeというメソッドがあります。これは、引数で指定したRangeをSelectionから除去するというものです。このように除去すべきオブジェクトを直接指定するものには、かつてremoveChild(二章第四回)がありましたね。

このメソッドの場合、getRangeAtで取得したRangeを引数に指定して削除することになるでしょう。

var sel = getSelection();
if(sel.rangeCount>0){
  //Rangeが最低一つある場合
  sel.removeRange(sel.getRangeAt(0));
}
        

しかし、このremoveRangeというメソッドはFirefoxでは実装されていますが、Chromeでは実装されていないようです(2014年7月現在)。これでは少し頼りないので、もう1つ紹介します。

それは、removeAllRangesです。これは、引数無しで、全てのRangeを除去するというものです。

sel.removeAllRanges();

Selectionの実践

さて、Selectionを使えば、Rangeの機能を活かすことができそうです。ちょっとやってみましょう。

しかし、今までのようにbody要素の最後にscript要素を書くパターンだと、ページを読み込んだ瞬間にそれが実行されてそれっきりです。

Selectionはブラウザが選択した範囲をいじるものですから、読み込まれた瞬間に実行されても選択できません。そこで、選択したときを見計らって実行する必要があります。そういうときに使うのが、そう、イベントです。忘れた人は、三章を見るとよいでしょう。

今回どういう時に実行すればいいかですが、ずばりマウスボタンが上がったタイミングがいいでしょう。つまり、選択し終わった直後ですね。

ここで使うのは、mouseupというイベントです。「マウスが上がったとき」ですね。逆に、mousedownもあります。これを使って、


document.addEventListener('mouseup',function(ev){


},false);
          

というようにします。さて、これを基本にやっていくわけですが、まずは簡単に、「選択しても消える」というのをやってみましょう。これは簡単で、mouseup時にremoveAllRangesを実行するだけです。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>

    <p>あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん</p>
    <p>あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん</p>
    <p>あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん</p>

    <script type="text/javascript">
      document.addEventListener('mouseup',function(ev){
        var selection = getSelection();
        selection.removeAllRanges();
      },false);
    </script>
  </body>
</html>
        

簡単にできました。しかし、これを見て「サイトにコピーして欲しくない文章があるから、これを使って対策しよう」とは思いませんよね。ソースを見れば簡単にコピーされるのですから。そんなことを思っているうちはまだまだ初心者なのです。

次に、「選択した部分がページから消える」というのを作ってみましょう。これも簡単です。前回、RangeのdeleteContentsを解説したので、選択部分のRangeを取得してこれを使えば一発です。

<script type="text/javascript">
  document.addEventListener('mouseup',function(ev){
    var selection = getSelection();
    if(selection.rangeCount > 0){
      var range = selection.getRangeAt(0);
      range.deleteContents();
      selection.removeAllRanges();
    }
  },false);
</script>
          

今回はRangeを取得して扱うので、マウスを押したけど選択していない場合を考慮し、rangeCountを比較してRangeが取得できる場合のみ処理をしています。

さらにさらに、もう少し工夫がいる例として、「選択した部分の背景色が変わる」というのをやってみましょう。

色を変えるには、スタイルシートを使うことになります。今回の場合、background-colorプロパティを変えればよいのです。

さて、これは今までのより少し複雑です。ちょっと考えてみましょう。

まとめて囲む

まず思いつくのが、background-colorを指定した要素を用意し、選択部分全部そこにいれてしまうというものです。例えば、汎用性の高いspan要素で囲むとすると、

abcde<em>fghijklmn</em>opqrstuvwxyz
   |---------------------|
          

というように選択したとすると、

abc<span>de<em>fghijklmn</em>opq</span>rstuvwxyz
          

となるということです。

実は、前回紹介しませんでしたが、まさにこれ用のメソッドがあります。surroundContentsです。引数に、新しい親となるノードを指定します。

<script type="text/javascript">
  document.addEventListener('mouseup',function(ev){
    var selection = getSelection();
    if(selection.rangeCount > 0){
      var range = selection.getRangeAt(0);
      //新しいspan要素を作る
      var newspan = document.createElement('span');
      //background-colorを設定
      newspan.style.backgroundColor="#ffff00";

      range.surroundContents(newspan);
    }
  },false);
</script>
          

テキストを適当に選択してみると、いい感じに背景が変わってくれます。

しかし、まだまだ不完全です。3つのp要素がありますが、2つ以上のp要素にまたがって選択すると、なんとエラーが起きます。このエラーはsurrondContentsが起こしたもので、原因はつまりこういうことです。

<p>.........</p><p>........</p>

のようにRangeで選択されているとき、これを囲もうとすると

<p>......<span>...</p><p>...</span>.....</p>

となります。これは明らかに文法違反ですね。このように囲むことができない場合には、エラーを出してくるのです。これは仕方ないことですが、surrondContentsを使う際は注意しましょう。

ちなみに、動作しないのは仕方ないにしても、いちいちエラーを出すのが格好悪いという場合には、「try-catch構文」(機会があれば紹介するかもしれません)を使えばなんとかなります。

そこで、surrondContentsを使わず手動でこの作業をやると、エラーを起こさずにやることができます。つまり、囲むという作業を分解すると、「抜き出す」「親で囲む」「親を戻す」という3つの作業に分解することができます。これをそれぞれやればいいのです。

まず抜き出すのはextractContentsを使います。戻り値はDocumentFragmentでした。このextractContentsは、どんな範囲であろうとうまく抜き出してくれます。

次に親で囲むのは、上と同じようにspan要素を作った後、戻り値のDocumentFragmentをappendChildするだけです。DocumentFragmentは「親がいない兄弟」のような感じなので、ここでも問題は起こりません。

最後に、insertNodeで抜き出した場所にspan要素を戻します。ここでは、隙間に新しい要素を埋めるだけなので、やはり問題は起こりません。

つまり、こうです。

<script type="text/javascript">
  document.addEventListener('mouseup',function(ev){
    var selection = getSelection();
    if(selection.rangeCount > 0){
      var range = selection.getRangeAt(0);
      //新しいspan要素を作る
      var newspan = document.createElement('span');
      //background-colorを設定
      newspan.style.backgroundColor="#ffff00";

      var df = range.extractContents();
      newspan.appendChild(df);
      range.insertNode(newspan);
    }
  },false);
</script>
            

これでも同じようなことができています。しかしさっきのようにp要素をまたいで選択すると、エラーは起きないものの、p要素が増殖してしまいます。もっとも、それ以前にもう1つ問題があるのですが。

p要素が増殖する原因は、extractContentsの動作にあります。といっても、これも仕方ないことです。

<p>............</p>......…
        |----------------…

というような範囲の状態でextractContentsをすると、開始点より左の部分も確かにp要素の中、右もp要素の中、ということを両立するために、

<p>.....</p><p>.......</p>......…
            |-------------------…

というようにp要素が分割されて抜き出されます。

右の部分をspanで囲んで戻すと、

<p>.....</p><span><p>.......</p>......…</span>

となり、1つだったp要素が2つに増えています。

ちなみに、「それ以前の問題」とは、そもそもspan要素の中にp要素が入っていることです。これはいけません。

さて、原因が分かったところで、これの解決は次回ということにします。