オンラインエディタのIME対応などのメモ
世の中に存在するオンラインエディタうち、textareaを直接使っていないものの大半が、適切にIME処理を出来ていない。IME周りの処理は厄介なので、とりあえずどういう感じなのかを調べた結果をメモしておく。後で直したい。
オンラインエディタの仕組み
オンラインエディタの主な目的はsyntax-highlightingとCSSによるカスタマイズである。textareaは、個々の文字の強調や色づけができない。
<div contenteditable="contenteditable">
まず始めに思いつくのがHTML5のcontenteditable属性で、これは任意の要素を編集可能にするもの。execCommandというコマンドがあって、これを使うと選択範囲の文字をボールドにしたりできて、リッチテキスト編集のためにあるものなので、一見便利そうである。しかし、キャレット(エディタ内の縦棒の点滅するカーソル)の操作が自由にできない、などの点に問題があり、広くは使われていない。
次に、非表示のtextareaと、divを使った方法。オンラインエディタの多くがこの手法を使っている。要するに、非表示のtextareaのvalueの変更を監視して、divに反映させるという手法である。divに表示するときにsyntax-highlightingなどをする。
一見うまくいきそうだが、厄介な問題がいくつかある。
キャレット
単純に、上下左右のキーコードを監視しているだけでは、文字をクリックしてキャレットを動かすことができない。 clickを監視して、どの文字をクリックしたのかを頑張って判別する必要がある。
文字のドラッグアンドドロップ
Aceは対応しているが、ほとんどのエディタは対応していない。キャレットを独自に実装している場合は細かい動作を再現することが難しい。基本的には、drag & dropのAPIと、上記のキャレットでの文字クリックの判別の手法を使えばできるはずである。
IME対応
textareaを非表示にするとき、position: absoluteにして画面外に飛ばすという手法を使っている場合、IMEの変換候補位置が、文字を編集している(ように見える)位置ではなく、textareaが存在する位置になってしまう(画面の右上に飛んだりする)。 そして、IMEで入力中でも、監視しているイベントによっては入力が完了するまで発行されないので、入力中に文字が見えなかったりする。 また、inputイベントなど、IME入力中にも発行されるイベントを監視していても、IME入力中であるアンダーラインが表示されない。 これはComposition Eventを使えば解決することができるのであるが、ブラウザ間の挙動の違いや、まあ色々なことがあってほとんど実装されていない。
Ace
割と良く出来てる。IME周りは独自実装をあきらめて、textareaを一時的に表示するように解決しているので(昔のTextMateなどのように)、ブログを書くなどの目的に使うには、少し辛い。
CodeMirror
IME周りは単にvalueをコピーしているだけなので、変換中であることを示すアンダーラインが表示されない。
CompositionEventとIME対応
textareaがあるとして、IME編集中の範囲を取得する方法はだいたいこんな感じでいけそう。textareaの選択範囲はselectionStart / selectionEndで取れるが、IME中は正しい数値が取れない。が、しばらくいじってみた結果、Math.minを使えばうまく調整できることを発見した。その方法を書いておこうと思う。
まず、Composition Eventを使って、編集中かどうかと編集中のイベントを保存する。textarea変数はtextarea要素が入ってる。
var composing = false; var lastCompositionEvent; textarea.addEventListener('compositionstart', function (e) { composing = true; lastCompositionEvent = e; }, false); textarea.addEventListener('compositionupdate', function (e) { lastCompositionEvent = e; }, false); textarea.addEventListener(('compositionend', function (e) { composing = false; lastCompositionEvent = e; }, false);
selectionStart / selectionEndをIME入力中にも正しく取得するにはだいたいこんな感じにすればいい。
var selectionStart = textarea.selectionStart; var selectionEnd = textarea.selectionEnd; if (composing) { selectionStart = Math.min(editor.selectionEnd - lastCompositionEvent.data.length, editor.selectionStart); selectionEnd = Math.min(editor.selectionStart + lastCompositionEvent.data.length, editor.selectionEnd); } if (composing) { // selectionStart / selectionEndが、入力中の範囲を指すのでunderlineを引けば良い } else { // selectionStart / selectionEndは通常の、選択範囲を指す }
試しに非表示textareaとdivを使って、カスタムキャレットとIME対応を実装してみた例。
まあまあうまくいってる。ドラッグアンドドロップを実装していないが、とりあえずIME対応が技術的に不可能ではなさそうなことが判明した。