◆ issues はあった
◆ Slack も使っているテキストエディタの Quill 側で発生してる問題
  ◆ これ使っていれば他のサービスも入力できないはず
◆ MutationObserver での処理中に問題が起きてる

Slack の日本語入力がおかしい件の続きです
今朝から Slack 使っていてやっぱり気になる! と思ってもうちょっと調べました

composition

devtools でアタッチされたイベントリスナを眺めていると compositionstart と compositionend イベントがありました
これは IME 関連のもので IME の変換可能な入力が開始されたときと 確定したりバックスペースで消したなどで IME の変換可能状態が終了したときに起きるイベントです

IME がおかしいので最初に調べるべきでした
でも Slack って英語 UI ですし 日本語など IME 環境のことなんて考えていないものだと思ってました
フォーカス変えるなどの処理が IME のことを考えていないから起きたものだと思ってました
前回書いた 「クローンしたら 2 つめに入力しても 1 つめに入力されてる」 という謎の現象は IME の有効無効関係なく起きてましたし

とは言っても ソースが開けないことに変わりありません (おもすぎて Chrome が耐えられない)
compositionstart でググれば出ないかなと軽くググってみれば Github の issues がありました
https://github.com/quilljs/quill/issues/2009

Quill

Slack は OSS ではないので別のソフトのようです
でも起きてる現象はまさに Slack と同じものです
今のところ報告のみで解決作などは書かれていません

同じ処理があれば同じように影響受けるよねと思ってこの Quill のページを開いてみたら

いきなり目に入る TRUSTED BY: に Slack のアイコン!


Slack って入力 UI に Quill を使ってたのですね
そういえば入力するところの DOM は ql-editor というクラスでした
ql は quill のことなのですね

quill はエディタのライブラリで自称は "powerful rich text editor" らしいです
TrustedBy には microsoft, slack, linkedin, gusto, reedsy, voxmedia, buffer, intuit, asana, mode, front, slab など聞いたことないですが新しそうな感じのサービスっぽく見えるアイコンがいろいろ並んでました
けっこう有名ドコロみたいです

再現できた

Slack ではおもすぎてソースが見れなかったですが Quill 単体で使えば十分 devtools で確認できそうです
やってみるとこのコードで再現できました

<!doctype html>

<link href="https://cdn.quilljs.com/1.3.5/quill.snow.css" rel="stylesheet">
<script src="https://cdn.quilljs.com/1.3.5/quill.js"></script>

<div id="editor-container"></div>

<script>
var quill = new Quill("#editor-container", {theme: "snow"})
</script>

イベントリスナじゃなかった

見た目は凄くシンプルですが Quill はけっこう大きめのライブラリでソースも長いです

どのイベントで起きてるのかを判断しようと devtools で徐々にリスナを削除しながら試していると……
全部消したのに 再現します

どういうこと??

そもそもバグなら一度リスナつけたら 消しても残り続けるなんてことがないとは言い切れない? と思ってライブラリのロード前に addEventListener を無効にしました

<script>
EventTarget.prototype.addEventListener = function(){}
</script>

ですが 変わらず発生します

とりあえずタイマも追加します
<script>
EventTarget.prototype.addEventListener = function(){}
setInterval = function(){}
setTimeout = function(){}
</script>
ダメです
ちゃんと日本語が打てません

いったいどうして……
DOM をまるごとコピーしてコピーしたほうで入力してみると問題なく入力できています

リスナでもタイマでもないのに入力時に処理できるものなんて……

あっっ!!!

MutationObserver!!!!!

そういえば Quill では textarea でなく contenteditable で DOM の中身を書き換えて入力します
入力のたびに DOM の node が更新されます
つまり MutationObserver を使ってテキスト入力時に JavaScript を実行できます

MutationObserver

さっそく MutationObserver を無効にしました
返り値を使ったりするので単純に空の関数に置き換えるのではなく 何もしない関数を設定した observer を返すようにします
これで observe しても何も起きずエラーも起きません

<script>
EventTarget.prototype.addEventListener = function(){}
setInterval = function(){}
setTimeout = function(){}

const MO = MutationObserver
MutationObserver = function(){return new MO(function(){})}
</script>

これで実行してみると ちゃんと入力出来るようになりました!
でも本来必要な処理を無視させてるので困ることもありそうです

それに Slack を動くようにするというより 「Chrome のアップデートでどこが変更された結果動かなくなったのか」 という方が興味あります

Firefox と比べる

手元に一つ前の Chrome 64 がなかったので Firefox と比べてみました
昔インストールした Chromium 60 はあったのですがなぜか Chrome 65 と同じ日本語がちゃんと打てない状態でした
64 では入力できていたので 60 で入力できないはずないと思うのですが Chrome と Chromium のバージョンは必ず同等の機能ではないのですし Chromium はあてにならなそうです

Quill で MutationObserver を使ってるのは一箇所のみでしたので そこに debugger を仕込んだりしてデータを比べます

行の最初の入力と最後の文字を消して行が空になったとき Chrome はこうなりました

行の最初の入力
childList NodeList [text] NodeList []
childList NodeList [] NodeList [br]
characterData NodeList [] NodeList []

行の文字を全部削除
characterData NodeList [] NodeList []
childList NodeList [] NodeList [text]
childList NodeList [br] NodeList []

type, addedNodes, removeNodes の順に 3 つならんでいます
それ以外のすでにテキストのある行の編集は characterData の変更のみで childList は変化なしです
行が空になると text node が削除されて変わりに br タグが挿入され 新しい行に入力すると br が削除され代わりに text node が挿入されるという動きです

Firefox だとこれが一点違っていて 行の最初の入力の 2 つめ br の削除が発生しません
最終的な DOM を見ると Chrome と同じように動いているのですがなぜか mutation が 1 つ少ないです

observer のコールバックが呼ばれるタイミングでは

[Firefox]
<p>あ<br></p>

[Chrome]
<p>あ</p>

という状態でした

適当に流れを追っていくと ContainerBlot.insertBefore が呼び出されて中では node の insertBefore が使われてすでに入力中の 「あ」 が追加されていました
そこに行く途中の条件に nextSibling が null のときだけになっていて Chrome では br がないので追加されてました

ということは Chrome 64 以前は Firefox と同じで br が残る仕様だったのでしょうか

Chrome 64

その後 Chrome 64 が使えるパソコンを使う機会があったのでついでに調べてみたのですが 上に書いた部分は Chrome 65 同じ動きでした
MutationObserver に渡される MutationRecord は 3 つですし br は残りません
やっぱり原因が謎です

コードも長くて疲れてきたのでこの辺にします
気が向けば続くかもしれません

[3/14] 追記

Quill の方ではこの記事を書いた翌日にはもう修正されていました
1.3.6 が出ています

上で書いたサンプルのバージョンを変えて
<!doctype html>

<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>

<div id="editor-container"></div>

<script>
var quill = new Quill("#editor-container", {theme: "snow"})
</script>
にすれば動きます
Slack の方はまだ対応されていませんが このモジュールのバージョンを上げれば修正されるのでもうすぐ修正されるでしょう


ところで 修正されたコードは quilljs/parchment の方のプロジェクトのこのコミットです
https://github.com/quilljs/parchment/commit/17f61235182bda64ba7535dab6ee5a68a4a807a9

insertBefore 系であってたようです

もうちょっと Quill の issues を見てると過去にもこの問題は何度か起きてるようです
例えば去年の 5 月にも
https://github.com/quilljs/quill/issues/1453

IME の問題だけあって中国語や韓国語の issues もあります
これが一番最初のものみたいです
https://github.com/surmon-china/vue-quill-editor/issues/56
ここの関連する issue を見てると過去の報告がいくつもあります
同じ時期のもありますが 何度も起きてるようなら今後もありえるかもしれないですね

[3/15] 追記

Slack の方も修正されました
昨日の夕方はまだ直ってませんでしたが 今朝にはもう直っていました

ライブラリ側が対応してすぐに対応されるのはいいですね
ものによっては修正するまでに何週間とか買ったりするものだってあるくらいですし