Slack の日本語入力がおかしい件の続き
- カテゴリ:
- JavaScript
- Webサービス
- コメント数:
- Comments: 1
◆ issues はあった
◆ Slack も使っているテキストエディタの Quill 側で発生してる問題
◆ これ使っていれば他のサービスも入力できないはず
◆ MutationObserver での処理中に問題が起きてる
◆ Slack も使っているテキストエディタの Quill 側で発生してる問題
◆ これ使っていれば他のサービスも入力できないはず
◆ MutationObserver での処理中に問題が起きてる
Slack の日本語入力がおかしい件の続きです
今朝から Slack 使っていてやっぱり気になる! と思ってもうちょっと調べました
これは IME 関連のもので IME の変換可能な入力が開始されたときと 確定したりバックスペースで消したなどで IME の変換可能状態が終了したときに起きるイベントです
IME がおかしいので最初に調べるべきでした
でも Slack って英語 UI ですし 日本語など IME 環境のことなんて考えていないものだと思ってました
フォーカス変えるなどの処理が IME のことを考えていないから起きたものだと思ってました
前回書いた 「クローンしたら 2 つめに入力しても 1 つめに入力されてる」 という謎の現象は IME の有効無効関係なく起きてましたし
とは言っても ソースが開けないことに変わりありません (おもすぎて Chrome が耐えられない)
compositionstart でググれば出ないかなと軽くググってみれば Github の issues がありました
https://github.com/quilljs/quill/issues/2009
でも起きてる現象はまさに 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 など聞いたことないですが新しそうな感じのサービスっぽく見えるアイコンがいろいろ並んでました
けっこう有名ドコロみたいです
やってみるとこのコードで再現できました
どのイベントで起きてるのかを判断しようと devtools で徐々にリスナを削除しながら試していると……
全部消したのに 再現します
どういうこと??
そもそもバグなら一度リスナつけたら 消しても残り続けるなんてことがないとは言い切れない? と思ってライブラリのロード前に addEventListener を無効にしました
ですが 変わらず発生します
とりあえずタイマも追加します
ちゃんと日本語が打てません
いったいどうして……
DOM をまるごとコピーしてコピーしたほうで入力してみると問題なく入力できています
リスナでもタイマでもないのに入力時に処理できるものなんて……
あっっ!!!
MutationObserver!!!!!
そういえば Quill では textarea でなく contenteditable で DOM の中身を書き換えて入力します
入力のたびに DOM の node が更新されます
つまり MutationObserver を使ってテキスト入力時に JavaScript を実行できます
返り値を使ったりするので単純に空の関数に置き換えるのではなく 何もしない関数を設定した observer を返すようにします
これで observe しても何も起きずエラーも起きません
これで実行してみると ちゃんと入力出来るようになりました!
でも本来必要な処理を無視させてるので困ることもありそうです
それに Slack を動くようにするというより 「Chrome のアップデートでどこが変更された結果動かなくなったのか」 という方が興味あります
昔インストールした Chromium 60 はあったのですがなぜか Chrome 65 と同じ日本語がちゃんと打てない状態でした
64 では入力できていたので 60 で入力できないはずないと思うのですが Chrome と Chromium のバージョンは必ず同等の機能ではないのですし Chromium はあてにならなそうです
Quill で MutationObserver を使ってるのは一箇所のみでしたので そこに debugger を仕込んだりしてデータを比べます
行の最初の入力と最後の文字を消して行が空になったとき Chrome はこうなりました
type, addedNodes, removeNodes の順に 3 つならんでいます
それ以外のすでにテキストのある行の編集は characterData の変更のみで childList は変化なしです
行が空になると text node が削除されて変わりに br タグが挿入され 新しい行に入力すると br が削除され代わりに text node が挿入されるという動きです
Firefox だとこれが一点違っていて 行の最初の入力の 2 つめ br の削除が発生しません
最終的な DOM を見ると Chrome と同じように動いているのですがなぜか mutation が 1 つ少ないです
observer のコールバックが呼ばれるタイミングでは
[Firefox]
[Chrome]
という状態でした
適当に流れを追っていくと ContainerBlot.insertBefore が呼び出されて中では node の insertBefore が使われてすでに入力中の 「あ」 が追加されていました
そこに行く途中の条件に nextSibling が null のときだけになっていて Chrome では br がないので追加されてました
ということは Chrome 64 以前は Firefox と同じで br が残る仕様だったのでしょうか
MutationObserver に渡される MutationRecord は 3 つですし br は残りません
やっぱり原因が謎です
コードも長くて疲れてきたのでこの辺にします
気が向けば続くかもしれません
1.3.6 が出ています
上で書いたサンプルのバージョンを変えて
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 を見てると過去の報告がいくつもあります
同じ時期のもありますが 何度も起きてるようなら今後もありえるかもしれないですね
昨日の夕方はまだ直ってませんでしたが 今朝にはもう直っていました
ライブラリ側が対応してすぐに対応されるのはいいですね
ものによっては修正するまでに何週間とか買ったりするものだってあるくらいですし
今朝から 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>
<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>
EventTarget.prototype.addEventListener = function(){}
</script>
ですが 変わらず発生します
とりあえずタイマも追加します
<script>
EventTarget.prototype.addEventListener = function(){}
setInterval = function(){}
setTimeout = function(){}
</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>
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 []
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>
にすれば動きます<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 の方も修正されました昨日の夕方はまだ直ってませんでしたが 今朝にはもう直っていました
ライブラリ側が対応してすぐに対応されるのはいいですね
ものによっては修正するまでに何週間とか買ったりするものだってあるくらいですし