前提
- CodeMirror 5.31.0 について記述しています。
- iPhone6s、iOS10.3.3、Mobile Safari で遭遇した問題について記述しています。
問題点
CodeMirrorには2つの入力モードが用意されており、それはinputStyle
というオプションで切り替えることができる。
なのだけれど、スマホから使用した時、日本語入力(というかIME)に関しては、この2つの入力モードのどちらでも何かしらの問題が発生する。
inputStyle = textarea
見えないテキストエリアを使用して入力を行う方式。
入力しているテキストエリアと、表示されているcontenteditableなエディタ部分の要素との違いが問題を発生させる。文章をタッチするとtextareaが起動してイベントを拾ってしまうため、文字選択ができない問題がある。
入力は問題なくできるように思えるが、IMEで確定前の文字を削除すると文字が二重で入力されたりする。
inputStyle = contenteditable
contenteditableの要素に直接入力する方法。直接といってもキー入力をエミュレートしてCodeMirror内で複雑なことをしているのだけれど・・。
この入力方式の場合は致命的な問題がある。なんとIMEがきかない。たとえば、「て」と入力した時点で「て」が確定されてしまい、「手」のような漢字に変換することができない。
まとめるとこうなる。つまりCodeMirrorを日本で使う場合は、モバイルでまったく使い物にならない。
textarea | contenteditable | |
---|---|---|
IME入力 | ○ | × |
IME入力中の文字削除 | × | × |
文字選択 | × | ○ |
絵文字キーボードでの入力 | ○ | × |
3Dタッチによるカーソル移動 | × | ○ |
この問題は本家でIssueにもなっている。
解決方法
イベントの流れをみると、どうやらIME入力中にkeypress
とかkeydown
が発生するのが主な原因だったようなので、IME入力中のkeypress
はキャンセルするようにした。
cm = CodeMirror.fromTextArea(textarea, {inputStyle: 'contenteditable'})
if(UserAgentがモバイルであれば) {
const origOnKeyPress = cm.display.input.onKeyPress // 元のkeypressを取得
cm.display.input.onKeyPress = function(e) {
// iOSの絵文字キーボードのように、サロゲートペアの直接のkeypressは、CodeMirrorの処理を通すと文字化けるのでキャンセルする
if(e.which >= 0x10000) {
return
}
if(!cm.display.input.composing) { // 文字入力中かどうかはcomposingでわかる
cm.keyPressTimer = setTimeout( () => {
origOnKeyPress.call(this, e)
}, 30) // 30ms以内(適当)にcompositionstartが呼ばれなければkeypressは実行して良い
}
}
const inputArea = cm.display.input.div || cm.display.input.textarea // 一応、inputStyleがどちらの状態でも大丈夫な書き方にした
inputArea.addEventListener('compositionstart', (_cm, e) => {
if(cm.keyPressTimer) {
clearTimeout(cm.keyPressTimer)
}
}, false)
const inputField = cm.display.input.getField()
// IME入力中のkeydownをCodeMirrorに渡さないようにする
window.addEventListener('keydown', function(e){
if(e.target == inputField && cm.display.input.composing) {
e.stopPropagation()
}
}, true)
// バーチャルキーボードの「完了」ボタンを押した時に、キーボードだけが消えないようにする
inputField.addEventListener('blur', function(e){
if(e.relatedTarget) { // 「完了」ボタンでのblurかどうか
return
}
e.stopPropagation() // stopしないと文字が二重で入力される
if (cm.display.input.composing) {
// 再focusが確定したらblurさせる
inputField.focus()
setTimeout(function(){
inputField.blur()
}, 1)
}
}, false)
}
残る問題点
とりあえずこれだけやれば日本語入力や文字の選択が普通にできた。のだけれど、IME入力中にエディタの内容が外部から書き換わった場合(CodeMirrorのAPIを使って書き換えたりした時)に、MobileSafariのタブ単位でエラーが発生し、タブが強制リロードされるという致命的な問題がある。
どうやらMobileSafariでは、contenteditableの要素に対してIMEで入力中に、対象の要素が削除されると入力した文字が行き場を失ってしまいエラーになってしまうみたいだった。
通常の使い方であれば、IME入力中にエディタの中身を書き換えるようなことはしないので大丈夫ではあるのだけれど、たとえば同時編集機能にCodeMirrorを使っていて、他のユーザからエディタ内の文字が書き換えられるようなシチュエーションでは問題になる。
これの解決方法は・・・・!! わからない。