ボクココ

サービス開発を成功させるまでの歩み

ContentEditable のハマりどころと対処法

ども、@kimihom です。

f:id:cevid_cpp:20171008154936p:plain

前回の記事で、ContentEditable についての概要をざっと書いた。ContentEditable の持つ魔力については以下の記事を参照いただきたい。

www.bokukoko.info

さて、本記事では実際に ContentEditable を使って実装しようと思った際に気をつけておいたほうがいい点をざっと挙げていく。もし今後 ContentEditable を使った実装をする方の参考になれば幸いだ。

タグ挿入後の罠

ContentEditable を使うなら、何らかのテキスト入力でリアルタイムに HTML タグの挿入をする(document.execCommand('insertHTML'))といった実装をしていくことになるだろう。 実際上記コマンドを実行するだけなら問題なく動作するが、問題はその後にやってくる。例えば <span> タグを動的に挿入した後、その直後に日本語入力をしようとすると、数文字打った後で変換カーソルから外れるという謎バグが発生する(Chrome version 61)。また、ContentEditable 内の<span>タグのの後に何も文字入力がない状態だと、キャレット( | 点滅の文字入力カーソル)が ContentEditable 外に飛ぶというバグも発生した。

ContentEditable の実装で例に挙げた Twitter のタイムラインの ContentEditable は、タグを入力するたびに ContentEditable 内を全てを書き換えるという何とも大胆な実装を施している。 Twitter の投稿文字が140字までという制約があるからこそ成せる芸当だ。実際、Twitter のテキストエリアにタグを100個以上挿入すると、Twitter テキストエリアの動作が極端に重くなる。

本問題に対し、私は<span>タグで何かを入れた後には必ず半角スペースを入れるという実装で対応した。つまりは keydown イベントで全ての対象 <span>タグを走査し、タグ直後にスペースがなければ document.execCommand('insertText', false, " "); を実行している。意図しないスペースが入るということでユーザーにはあまりそのことを気づかないような UX にすることに苦労した。他の方法で対応できた方がいれば是非教えていただきたい!

keydown と keyup の罠

Twitter のように # 入力でタグの候補が出てくるような何かを実装したい場合、keyup でイベント補足をしたいと思うことだろう。そうすれば、#を入力した後のイベントとしてコードを書くことができるので、動的な HTML の書き換えが楽に対応できるためである。

しかし、keyup イベントで # の入力を保障しようとすると問題が発生する。なぜか keyup では #を素早く入力した場合に keyup# の入力を取ってこれないことがあるのだ! この致命的な問題に対応するには、 keydown の時点で # 入力を補足するようにするしかない。この問題により、keyup 時点での実装とは異なった実装の工夫が必要になってしまった。

このように、keydown と keyup, keypress などのイベントの違いを理解した上で、適切な実装が必要になってくる。当然素早いキー入力や記号の入力など、実際に入力されるであろう文字列を全て考慮した上で実装しなければならない。

キャレットの罠

テキストエリア入力なら当然のようにあるキャレット (| で現在のフォーカスを示すやつ) だが、ContentEditable で最も苦戦する機能実装の1つといっても過言ではないだろう。

例えば Twitter のタグ # 入力後に 選択肢が出てきて、そのうちの1つを選択した後にはタグを確定して入力カーソルをそのタグの外から始めるという実装にしたいという場合を考えてみる。この場合、キャレットは新規で作ったタグ外側に合わせるという実装をすることになる。

そこで Range ってのを使うことになる。例えば以下のようなコードだ。

      var range = document.createRange();
      range.setStartAfter(tag);
      var sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);

この Range は、キャレットを調整するための色々なメソッドを用意しているので試行錯誤しながら調整をしていくことになるだろう。ここら辺の実装をしている時の、ContentEditable でドツボにハマっている感はなかなかエキサイティングなのでぜひ楽しんでいただければ幸いだ。この Range の動作を工夫することで、違和感のない心地よい UX を ContentEditable で実現できる。

ContentEditable 内の子タグで keydown等のイベントを補足できない

色々と ContentEditable の実装を試行錯誤していると、例えば ContentEditable 内の特定の <span> タグでのみ keyup イベントを補足したい みたいなアイディアが浮かんでくる。残念ながらこの方法は実質不可能である。(ContentEditable 内に ContentEditable=false タグを入れてさらにその中にある ContentEditable=true の要素の keyup イベントは取れるが、実装の解として適切ではない)

そのため、ContentEditable 自体の keyupkeydown イベントだけを使って、キー入力を最適化していくことになる。その事実を知った時の絶望感は半端なかった。子タグ内でイベント捕捉できたら実装がどんなに楽になっていたことか。つまり、やるなら Twitter のように、ContentEditable 内の対象タグを毎回チェックして実行する というようなコードを書くことになるだろう。この実装をするのには躊躇したが、検討した結果この方法しかなかった。本件に関しても、他に方法があれば是非教えていただきたい。

そして最後の砦、 IME

英語だったらこんな問題が起きないのだけど、日本語入力の場合は変換という作業がある。ご想像の通り、keydown 等のイベントは、日本語入力時にも全て取ってきてしまう。ここで何が問題になるのかというと、Enter の挙動だ。

日本語入力時の確定Enter では何も起こさず、その後の確定 Enter の際にのみ実行したいといったことが出てくる。全ブラウザでこの挙動に対応するには、結構面倒な手続きが必要だ。これはもう日本語を扱う我々にとって宿命だと思ってやるしかない。以下のリンクを参考にしていただきたい。jQuery じゃなくても参考になる。

jQueryでIME入力確定時にイベントを発行する - Qiita

当然、IME 以外にもクロスブラウザの問題は当然のように出てくる。ブラウザごとのバージョンアップなども追従する必要があるので、この実装がどんなに大変かは簡単にご想像できることだろう。

終わりに

今回は私が遭遇した ContentEditable の実装の一部だけではあるがご紹介した。

前回の記事でも書いたが、改めて ContentEditable を利用する際には、本当に今やりたい実装は ContentEditable が必要か? を考え直してみることをオススメする。以前、私はこんなツイートをしていた。

実際にContentEditable を実装してみて、Markdown の手軽さを痛感した次第である。

本当に ContentEditable が必要なタイミングで、この苦難を乗り越えた先に、最高の UX を提供できるサービスを実現できるはずだ。