Home About Contact
JavaScript , Note Taking

簡単なエディタをつくる試みを開始 contenteditable を調べる

ウェブ上で動くツールでメモを書いているのだが、 テキストエディタの部分を改良したいと思い始めた。 現状は Ace を使ってプレーンテキストにマークアップ(マークダウン)しているだけ。 これを Google Doc のようにできないか。 もちろん、そんなワープロのような高度なことをしたいわけではなく、 もし実現できたとしてこのエディタに期待することは・・・

そんな程度の話である。

もちろん、そんな程度の話だとしても、別に実装が楽にできるとは思っていない。

調べてみると、HTML Canvas でプリミティブに実装する、 ContentEditableを使う、lexical のようなライブラリを使う、という方法が見つかった。 (もちろん、ほかにも方法はあるだろう。)

日本語入力の問題がなければ、HTML Canvas 上に実装するのがよさそうに思った。

Core HTML5 Canvas に 実装方法の説明がが載っている。

しかし、日本語入力(Input Method Editor)との連携する方法がわからなかった。 それに近い方法として、入力は HTML Input 要素を使い、描画は HTML Canvasを使う方法 (Canvasの上にテキスト領域を出現させる方法)を試したのだが、その限りにおいて、 これもアスキー文字だけならば上部にオーバーレイさせたテキスト領域と Canvas の描画位置を完全一致させることが可能かもしれない。しかし、日本語文字も含めると描画位置を完全一致させるのは不可能に思えた。そこで Canvas 方式はあきらめて contenteditable を試すことにした。

contenteditable

ネットの情報を見る限り 結局 lexical のようなブラウザの差異を吸収してくれるライブラリが必要なのかと思いつつ。 まずは contenteditable の仕組みを知らないで把握しよう。

index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>A Simple ContentEditable</title>
    <style>
      body {
        margin:0;
        padding:0;
        background-color: #eee;
      }

      #ce {
        margin: 0;
        padding: 0;
        width: 300px;
        height: 200px;
        font-size:12pt;
        font-family: Arial, sans-serif;
        border: 1px solid rgba(0,0,0,0.2);
      }

      #ce:focus {
        outline: none;
      }

      button {
        margin-top: 1em;
      }
    </style>
  </head>
  <body>
     <div id='ce' contenteditable='true'>Hello, World!</div>
     <button id='button'>Show this DOM</button>
     <script src='contenteditable.js'></script>
  </body>
</html>

contenteditable.js:

var button = document.getElementById('button');
button.onclick = function(e){
    var ce = document.getElementById('ce');
    console.log(ce);
};

index.html と contenteditable.js を同じフォルダに入れて、 index.html を Chrome で表示させたところ。 (環境は Chromebook を使用。)

Show this DOM ボタンをクリックすると、contenteditable 指定した div 要素をコンソールに出力。

contenteditable-1

contenteditable 領域を編集して、再度 Show this DOM ボタンをクリックしたところ。

contenteditable-2

ブラウザ上で該当要素内を編集すると その部分の DOM がダイナミックに変化する仕組みだった。

Hello,&nbsp
<div>World!</div>

このような仕組みなので、書き変わったDOMをJSでパースして何か処理してやればよい、ということらしい。 contenteditable.js を少し修正してみる。

このように取得したHTML要素を手動でパース。

var button = document.getElementById('button');
button.onclick = function(e){
    var ce = document.getElementById('ce');
    console.log(ce.constructor.name); // HTMLLDivElement
    console.log(ce.childNodes); // NodeList
    console.log(ce.childNodes[0]); // Hello, World!
};

実行した結果(コンソール)。

contenteditable-3

これは index.html を読み込んだ直後 contenteditable 要素をいっさい編集しない状態で、 Show this DOM ボタンをクリックした結果。

編集後では、このコードでは変更内容全体をパースできないのは言うまでもない。

入力を観察して介入する

たとえば、行頭が # + 半角スペース ならその行を太字にする、というような処理を入れたい、とする。 その場合は、入力イベントがおきて contenteditable 内の DOM が変化したら、そこで処理を入れたい。 そのために使うのが oninput

ce.oninput = function(e) {
    console.log(e);
};

または addEventListener でもよい。

ce.addEventListener('input', function(){
    console.log(e);
});

これで変更を監視して、変更されたら即座にそのDOMの内容を作り替えることで(ここでは太字にする)対応できそう。

自前実装の問題点

現在 HTML の初期状態が次のようになっている。

<div id='ce' contenteditable='true'>Hello, World!</div>

それを p 要素で包むという変更を加える。

<div id='ce' contenteditable='true'><p>Hello, World!</p></div>

HTMLのレンダリング表示上は変化は知れているが、DOM構造は変わるのでパーサーをそれに対応できるようにしなければいけない。そしてこのような初期状態から編集を開始すると、同じ変更操作(たとえば、Hello のあとで改行)を行ったとしても、変更後のDOMは p 要素を入れる前とは異なる。 そしてさらにブラウザごとにどうDOMが変化するかは異なるらしい。

これが lexical のようなライブラリが必要になる理由、とのこと。 Chromebook でしか使わないとかであれば、ライブラリに頼らないという選択もありかも。