rangeとselectionを使ってtextarea・WYSIWYG用iframe等のテキスト・HTMLを取得・置換する方法まとめ

Javscriptの開発において、1つ大きな障害となるのはクロスブラウザです。その中でも、rangeやselection関連のクロスブラウザ対策には辟易します。
毎度検索エンジンで調べている気がしたので、今回はこれらをまとめてみようと思います。

jQueryの利用を前提としていますが、限定的な利用なので、例えば$("textarea")[0]はdocument.getElementsByTagName("textarea").item(0)などで置き換えらえます。DOM要素を指定している個所は、利用シーンに合わせて変換してご利用ください。

rangeとselectionを使ってテキストを取得する方法

テキストを取得する対象の要素createRange可否処理内容
平文(textarea/iframe以外)使えるなら(旧IE系)→var range = document.selection.createRange();
var txt = range.text;
使えないなら→var range = window.getSelection().getRangeAt(0);
var txt = range.toString();
textarea使えるなら(旧IE系)→var range = document.selection.createRange();
var txt = range.text;
使えないなら→var ta = $("textarea")[0];
var val = ta.value;
var start = parseInt(ta.selectionStart, 10);
var end = parseInt(ta.selectionEnd, 10);
var txt = val.substring(start, end);
designMode=onのiframe使えるなら(旧IE系)→var range = $("iframe")[0].contentWindow.document.selection.createRange();
var txt = range.text;
使えないなら→var range = $("#iframe")[0].contentWindow.getSelection().getRangeAt(0);
var txt = range.toString();

txtの取得がゴールです。
旧IE系のcreateRangeの便利さが光ります。全て同じやり方で取得できます。
一方、旧IE系以外はtextareaだけが取得方法が大きく異なります。
実際のコードは下記のように分岐してご利用ください。

if(typeof(document.selection) != "undefined") {
var range = document.selection.createRange();
var txt = range.text;
} else if(typeof(window.getSelection) != "undefined") {
var range = window.getSelection().getRangeAt(0);
var txt = range.toString();
}

尚、IE9~10は、getSelectionとcreateRangeの両刀使いのため、注意しないとバグの温床となります。
今回のテーマでは、createRangeが優秀なので、基本的にdocument.selectionの判定を先に行って、createRangeを優先させるのをオススメします。

rangeとselectionを使ってHTMLを取得する方法

テキストを取得する対象の要素createRange可否処理内容
平文(textarea/iframe以外)使えるなら(旧IE系)→var range = document.selection.createRange();
var html = range.htmlText;
使えないなら→var range = window.getSelection().getRangeAt(0);
var div = $("<div></div>").html(range.cloneContents() );
var html = div.html(); 
designMode=onのiframe使えるなら(旧IE系)→var range = $("iframe")[0].contentWindow.document.selection.createRange();
var html = range.htmlText;
使えないなら→var range = $("#iframe")[0].contentWindow.getSelection().getRangeAt(0);
var div = $("<div></div>").html(range.cloneContents() );
var html = div.html(); 

変数htmlの取得がゴールです。
HTMLを扱わないtextareaを除外しているため、旧IE系とそれ以外の両方とも似たようなコードになっています。
range内をcloneContentsで一時的なdivに設置してから中身を取得している旧IE系以外に対し、range.htmlTextで一発取得できるcreateRangeのスマートさがここでも光ります。

rangeとselectionを使ってテキストを置換する方法

テキストを取得する対象の要素createRange可否処理内容
textarea使えるなら(旧IE系)→var range = document.selection.createRange();
range.text = txt_replace;
使えないなら→var ta = $("textarea")[0];
var val = ta.value;
var start = parseInt(ta.selectionStart, 10);
var end = parseInt(ta.selectionEnd, 10);
var txt_before = val.substring(0, start);
var txt_after = val.substring(end);
ta.value = txt_before + txt_replace + txt_after;
designMode=onのiframe使えるなら(旧IE系)→var range = $("iframe")[0].contentWindow.document.selection.createRange();
range.text = txt_replace;
使えないなら→var range = $("iframe")[0].contentWindow.getSelection().getRangeAt(0);
$("iframe")[0].contentWindow.document.execCommand("insertHTML", false, txt_replace);

txt_replaceは置換後の文字を指します。
createRangeが使えない場合、textareaでは、取得した選択始点・選択終点から前後の文字を取得し、すべてを繋ぎ合わせてtextarea内を全て置換するという力技です。一方designModeでは、execCommandを利用しています。
createRangeが使える場合は、1行で済んでいてとてもスマートです。

まとめてみるとcreateRangeの優秀さが分かった

とにかくcreateRangeが使いやすくて便利です。
しかし、IE独自仕様のため、getSelectionとの併用となったIE9~IE10を経て、IE11では使用ができなくなりました。
本件に関しては、他のブラウザがIEの仕様に歩み寄ってほしかった、というのが正直な感想です。

まめわざでは、WYSIWYGエディターを自社開発して利用しています。
例えば、テーブルのセル1つ1つがWYSIWYGエディターとして動作します。これにより、WYSIWYGエディター内でテーブルを編集した場合とは大きく異なる操作性を実現しています。
WYSIWYGエディターであることを意識させないために、右クリックによってエディターを起動させる方法を採用しています。
また、Enterキーによって段落が分かれる(隙間が空く)従来のWYSIWYGエディターとは異なり、textareaのような操作感を実現しています。
必要であればリンクやテキストの彩色・拡大は可能、という「必要のない人には気付かせず、必要な人には機能を提供する」ことを目指したインターフェースです。
尚、文字の過度な彩色や拡大は、判読性を下げ(読みづらくなり)、デザイン性を損なう可能性があることを常に意識しましょう。

2015/7/1