WYSIWYGエディタっぽいものを自作してみようじゃないか

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
この画像、ちょっと気に入ってるんで使い回して行きます

この記事を三行にまとめると

ドラクエ3なら、そろそろロマリアに辿り着くかな〜って辺り
たぶん「何この人?」って思われるから
もはやこのブログでは恒例となりつつある「IEEEEEEEE!!」
普段、ブログを書いている人などは、WYSIWYGっていう機能を使う機会があると思います。ボタンをクリックするだけで文章の中にHTMLタグを埋め込んだりできる、あれです。このブログもWordpressで動いてますんで、WYSIWYGの世話になってます。

もともとWYSIWYGが入ってるブログツールなんかはともかくとして、自分でウェブサイトを作るとき、このWYSIWYGを入れたい。そんなときはどうするか。

探せばWYSIWYGエディタっていくつもあるんで、それ使えば良いっちゃ良いんですが、そこまで大層な機能じゃなくても良い、一つか二つHTMLタグを使えるようになれば良い、余計な機能があるとユーザーを惑わすから、最小限のものだけ欲しい、あるいはいざって時に自分でカスタマイズできるようにしておきたい、そういう場合もあるかもしれません。

そこで、簡単なものなら思いきって自作してみようじゃないかっていうのが今回の趣旨です。

複雑な機能をつけようとしなければ、実装はそんなに難しくないです。そう言いつつも僕は実装するのに結構手間取っちゃいましたが、それは僕のプログラマレベルが8くらいだからです。ドラクエ3なら、そろそろロマリアに辿り着くかな〜って辺り。まだまだこれからです。



strongタグを埋め込んでみる

何はともあれ、とりあえずjavascriptを書いてみたいと思います。jQueryを使った書き方しちゃいますが、jQuery使わなくてもほぼ同じなんでその辺はよしなに。

まずは最もよく使う……かどうかは分かりませんが、strongタグを入れてみましょう。ちなみに僕はあまり使わないなぁ……自分自身がストロングな男じゃないから?

<div style="margin:20px">
  <div><button id="btn-strong" class="btn" type="button">strongタグ</button></div>
  <div><textarea cols="100" rows="10" id="textarea"></textarea></div>
</div>
<script>
$(function(){
  $('#btn-strong').click(function(){
    //挿入先のテキストエリア
    var $textarea = $('#textarea').get(0);

    //選択範囲の開始位置と終了位置を取得
    var $len = new Object();
    $len.start = $textarea.selectionStart;
    $len.end = $textarea.selectionEnd;

    //テキストエリアに現在入っている文章
    var text = $textarea.value;

    //テキストをタグの挿入部分より前と挿入部分、挿入部分より後に分解
    var before = text.slice(0, $len.start);
    var select = text.slice($len.start, $len.end);
    var after = text.slice($len.end);

    //挿入部分の前後にタグを入れる
    $textarea.value = before + '<strong>' + select + '</strong>' + after;    
  });
});
</script>

以上です。これだけあれば、指先一つでテキストエリアの中にstrongタグを入れられるようになります。

サンプルページ

やっていることとしては、テキストエリアに入力されている文字列を「選択範囲より前」「選択範囲」「選択範囲より後」に一度分解して、選択範囲の前後にstrongタグを挿入したテキストを入れ直しているって感じです。選択範囲がない場合は、現在フォーカスされている場所(カーソルが当たってる位置って言った方が正しいのかな?)に、タグが埋め込まれます。

選択範囲の開始位置っていうのは選択範囲より前と選択範囲の間のことです。終了位置は選択部分と選択範囲より後の間。それぞれ文字列の先頭から何文字目かってのを、数字で取得しています。ただし先頭は0番目なので、例えば選択範囲の前後と選択範囲が「あいうえお」「かきくけこ」「さしすせそ」と分かれていた場合、「か」は6文字目ですが、開始位置は「5」となります。同様に終了位置は「10」となります。

何となく突貫工事みたいなやり方ですが、でもこの程度で良ければ、10行くらいで実装できちゃいます。平野レミさんがTwitterで料理のレシピを紹介するようにお手軽……ではないですが、それでもまあ、わりとお手軽じゃないでしょうか。

WYSIWYGっぽい感じにやってますが、実際のところは自分で好きな文字列を挿入しているだけなので、HTMLタグに限らず何でも入れられます。URLを入れることもできるし、俳句を挿入することもできるし、任意のメッセージを入れることもできる。「ロードローラーだっ!!」って文字を入れることももちろん可能。

素直に告白できない人はボタンを押したら「I love you」というテキストが入るようにしといて、意中の人に「このボタンを押してごらん? それが僕の君に対する気持ちさ」ってやってみたら? たぶん「何この人?」って思われるから。



リンクを埋め込んでみる

さっきURLを入れても良いと言いましたが、実際にaタグをURLつきで埋め込んでみましょうか。

$(function(){
  $('#btn-link').click(function(){
    //URLを入力
    var url = prompt('URLを入力してください。','');

    //挿入先のテキストエリア
    var $textarea = $('#textarea').get(0);

    //選択範囲の開始位置と終了位置を取得
    var $len = new Object();
    $len.start = $textarea.selectionStart;
    $len.end = $textarea.selectionEnd;

    //テキストエリアに現在入っている文章
    var text = $textarea.value;

    //テキストをタグの挿入部分より前と挿入部分、挿入部分より後に分解
    var before = text.slice(0, $len.start);
    var select = text.slice($len.start, $len.end);
    var after = text.slice($len.end);

    //挿入部分の前後にタグを入れる
    $textarea.value = before + '<a hrer="' + url + '">' + select + '</a>' + after;    
  });
});

さっきとほとんど一緒ですね。違いは、ボタンを押すとURLを入力するボックスが出てくるってだけです。サンプルページにある「aタグ」ボタンを押したときの動きがこれになります。

サンプルページ

これも、別にURLの入力に限定される機能というわけではなくて、任意に入力された文字列を挿入するって感じです。自動的に「I love you」と挿入されるボタンの横にこのメッセージ入力機能つきのボタンを用意しておいて、「僕の気持ちを読んだら、君の返事をここに書いてくれないか?」みたいなこともできるわけですね。俺ならやらないけど。



IE8の場合

もはやこのブログでは恒例となりつつある「IEEEEEEEE!!」の話。

細かく調査したわけではないんですが、少なくとも上記のコードだと、IE8じゃ上手く動きません。9とか10ならたぶん大丈夫。

どの辺が上手く動かないかっていうと、「$len」のとこ。フォーカスされてる位置に挿入するだけならできるんだけど、選択範囲の前後に入れようとするとどうにも上手くいかない。なので、IE8の場合はちょっとコードを変える必要がある。

変えるとこだけ書くと、こんな感じ。

var $len = new Object();

if(IE8以外) {
  $len.start = $textarea.selectionStart;
  $len.end = $textarea.selectionEnd;
} else {
  $textarea.focus();

  //選択範囲を取得
  var range = document.selection.createRange();
  var clone = range.duplicate();
  clone.moveToElementText($textarea);
  clone.setEndPoint('EndToEnd', range);

  //開始位置と終了位置
  $len.start = clone.text.length - range.text.length;
  $len.end = clone.text.length;
}

結果は一緒です。これならIE8でも動く。if文のとこ手を抜いちゃいましたが、許してヒヤシンス。





ちなみに……

IE8だけを分けるなら良いんですが、判定の仕方次第では、IEとそれ以外という分け方をする人もいるかもしれない。8だけとかあれだから、もうIEはバージョンに関係なく十把一絡げで良いよって人なら、こんな判定をするかもしれない。

IE = navigator.userAgent.toLowerCase().match(/msie ([\d.]+)/) ? true : false;

こういう判定だとIEであればtrueが返ってくるので、9とか10も8と同じ方の処理を使わなければならない。

その場合は、さらにもうちょっとだけコードに手を加える必要がある。

var $len = new Object();

//IE判定フラグ
IE = navigator.userAgent.toLowerCase().match(/msie ([\d.]+)/) ? true : false;

if(!IE) {
  $len.start = $textarea.selectionStart;
  $len.end = $textarea.selectionEnd;
} else {
  $textarea.focus();

  //選択範囲を取得
  var range = document.selection.createRange();
  var clone = range.duplicate();
  clone.moveToElementText($textarea);
  clone.setEndPoint('EndToEnd', range);

  //改行コードが入るとlengthの値が微妙に変わるので別個に計算して足す
  var tmpLength = clone.text.replace(/\r\n|\r|\n/g, "").length;
  var nLength = clone.text.replace(/\r\n|\r/g, "\n").split("\n").length - 1;
  var originLength = tmpLength + nLength;

  //開始位置と終了位置
  $len.start = originLength - range.text.length;
  $len.end = originLength;
}

僕がやってみた限りだと、IEはバージョンによって挿入される改行コードが違うのか、$lenの値がずれることがある。その結果変な位置にタグが挿入されてしまうので、改行コードを除去した長さなどを使って処理するようにしてます。

僕のやり方が何かおかしいってだけかもしれないけど、もし位置がずれるようなことがあれば、上記のように、改行抜きによる計算をしてみたら解決すると思います。






今回はstrongタグとaタグだけやりましたが、同じ要領で他のタグも埋め込めるようにすれば、自分なりにカスタマイズしたWYSIWYGエディタもどきを作成できます。よほど難しいことをやらない限りは、開発工数もそんなにかからないと思いますし、自作したものなら何かあっても改修しやすいというメリットは、あるような気がします。

個人的にはIE8はもういいってことにして、後半のコードは考えなくても良いと思うんですけどねぇ……でもまあ、一応、念の為に載せとくってことで。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください