LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


Web フォントを使って contenteditable から脱出する

こんにちは、LINE フロントエンド開発センターの玉田です。突然ですが、本日よりフロントエンド開発に携わる UIT のエンジニアが持ち回りで記事を公開する「UIT 新春 Tech blog」を開催します。

UIT のメンバーが普段の業務で得た知識や、年末年始でたまった知見などを共有していきます。本記事の公開から 1 月 28 日 (金) まで、平日の毎日違うメンバーが記事を公開していきます。ぜひ最後まで見に来てください!

トップバッターは私から、フロントエンドエンジニアを悩ませる contenteditable からの脱却についてです。

contenteditable の呪い

みなさんは contenteditable についてご存知でしょうか? contenteditable 属性とは、ある要素が編集可能かどうかを示す HTML 属性で、この属性を true にすることでユーザー自身がその HTML 要素を編集できるようにするものです。<input type="text"> や <textarea> といった要素はテキストを編集するのに対し、contenteditable な要素は画像やアンカータグといったアイテムを追加・削除する編集機能を実現できるため、ブログをはじめとしたユーザーによる投稿画面に多用されています。例えば、Twitter のツイートフォーム、Facebook の投稿フォームには contenteditable が使われています。

私が開発に携わっている LINE公式アカウントの管理画面 (LINE Official Account Manager) でも、LINE公式アカウントにおける「メッセージ配信」や「応答メッセージ」などの編集フォームに contenteditable が用いられています。これにより、テキスト中に LINE 独自の絵文字を埋め込んだり、アカウント名などのプレースホルダーを表す機能を実現しています。

しかし、contenteditable はその便利さの一方で多くのバグを生み出すきっかけとなる、フロントエンドエンジニアにとっての鬼門ともいえる機能として知られています。その理由は、テキストエディターが持つ複雑性によるものです。

  • キーボード操作への対応: 矢印キーでカーソルを移動したり、Ctrl+Z キーで操作を取り消したりやり直したりする Undo/Redo 機能は、テキストエディタとして基本的な機能です。しかし、contenteditable 自身の仕様のあいまいさにより各ブラウザの挙動はまちまちです。開発者は独自実装でキー操作に対応するしかないため永遠に苦しみます。
  • クリップボードへの対応: 編集内容がプレーンテキストに限定されている <textarea> などとは異なり、contenteditable な要素は HTML で表せるものなら何でもコピー&ペーストできます。当然外部から不要なものをペーストできないように何らかの対策が必要ですが、そのための対応およびペースト時のカーソル処理などなどで開発者は永遠に苦しみます。
  • 日本語 IME への対応: 上記のような問題を回避した OSS ライブラリは存在するものの、残念ながら日本語 IME の対応に不備があることも多く、そのようなライブラリが使えるケースは少ないです。そのため開発者は自力でこれらの問題に対応しなければならず永遠に苦しみます。

これらの問題は LINE Official Account Manager だけの問題ではなく、LINE のフロントエンドエンジニア共通の課題としてあり続けています。過去に公開された LINE BLOG アプリのエディタを紹介する記事では、contenteditable に対するカーソルの考え方について非常に詳しく紹介されています。
https://engineering.linecorp.com/ja/blog/contentable-development-of-line-blog-apps/

ただ、テキストの装飾や画像を埋め込む場合であればともかく、はたして今回のようにテキストに決まった要素を入れるためだけに contenteditable を使わなければならないのでしょうか? エディタ周りの度重なるバグ報告とそれに対する Dirty hack の末に、私たちのチームは contenteditable を使わずに同等の機能を実現する方法を模索するようになりました。

textarea overlay のアイディア

この問題に対して、LINE公式アカウントのチャット機能を開発するチームがある方法で解決しました。以下のスクリーンショットは、他のユーザーとのトークルームのやり取りを Web 上で実現する、LINE Official Account Manager と似た機能を有していますが、この入力フォームには contenteditable が使われていません。一体どのように実現しているのでしょうか?

実はこの入力フォームは 2 つの要素で構成されており、下層には一般的な <textarea> が、上層にはオーバーレイとなる <div> が同じ大きさで重ねられています。そして、絵文字として見えている部分には、textarea 上では全角スペースが挿入されており、オーバーレイの部分で絵文字を表示させています。textarea とオーバーレイのテキストが一致していれば、ちょうど全角スペースの上と重なる部分にうまく絵文字が配置されるという仕組みです。

ただし、この方法は 2 つ欠点があります。

一つ目は、絵文字のプレースホルダーとして全角スペースを用いている点です。このままの方法では、全角スペース幅以外の要素を挿入できません。また、ユーザーがブラウザに独自フォントを設定していた場合、フォントの全角スペース幅によっては位置が崩れてしまいます。

二つ目は、テキスト自体に絵文字に関する情報が含まれていない点です。textarea の内容は単なる全角スペースなので、どの位置の全角スペースがどの絵文字に該当するかの情報を適切に保持し、textarea の更新に対してハンドリングする必要があります。コピー&ペーストができるようクリップボードに絵文字の情報を含める処理も必要です。

というわけで、この textarea overlay のアイディアを引き継ぎつつ、Web フォントを使って上記の問題を解決する方法を検討します。

空白の文字を持つフォント

これまでの情報をまとめると、私たちがほしいものは「任意の大きさの要素のプレースホルダーとなる、画面上に表示されない文字」です。そのような都合の良いものを提供するのにうってつけのものが Web フォントです。つまり、「ある要素と同じ大きさを持つ空白の文字」をフォント内に用意すればよいということです。

もう少し詳しく理解するために、フォントファイルの構造について紹介します。Web フォント (WOFF/WOFF2) の元となる OpenType や TrueType のフォントには、実際に画面に表示される文字の形状データの他に Unicode のコードポイントと形状データをマッピングする CMap というデータが含まれています。一般的なフォントは、U+41→「A」、U+3042→「あ」のようにそれぞれのコードポイントに対応する形状データがマッピングされていますが、実はこの値はフォントごとに自由に設定できます。

このテクニックを活用しているものの一つが、Font Awesome などのアイコンライブラリの Web フォントです。Unicode のコードポイントの中には、私用領域 (Private Use Area) と呼ばれる自由に使える文字があり、このコードポイントにアイコンを紐付けることで、既存の文字と重複せずにアイコンを使うことができるようになります。

全く同じ方法で、空白の領域を持つ Web フォントを作っていきます。つまり、用意したいプレースホルダーに合わせて、私用領域のコードポイントと空白の形状データを紐付けた Web フォントを用意します。これにより、全角スペースなどの他の文字と共存したまま textarae に任意のスペースを作り出すことができます。

Web フォントを自作する

Web フォントを自分で作成する方法は複数ありますが、ベクターデータから変換する方法がもっとも一般的でしょう。ただ、今回は特に形状を持たない何種類かの幅の空白が表現できればよいので、気軽に各種フォーマットのフォントファイルを作成できる FontForge を用いることにしました。

埋め込み要素の種類一つにつき一つのコードポイントを割り当てる必要があるため、絵文字の場合 1024 個 (U+E000 ~ U+E3FF) のコードポイントを割り当てます。絵文字は正方形で全角一文字の大きさであることが事前に分かっているため、全角一文字の幅 (=1024) を設定します。絵文字以外の埋め込み要素のためにも同様に様々な幅を持つ空白を作成し、それぞれコードポイントに割り当てていきます。たとえば、全角 3 文字分の空白を表現するために、幅 3072 (=1024 * 3) を設定します。一通りコードポイントの割り当てを終えたあと、ブラウザで利用するために WOFF 形式で書き出します。

そして、作成したフォントを Web フォントとして読み込み、textarea の font-family に設定します。これまでに紹介したものと同じ方法で textarea と overlay としての div 要素を重ねて表示し、textarea の方には要素を埋め込みたい部分に私用領域の文字を追加します。これにより、textarea 上ではあたかも埋め込まれる要素を避けるかのようにスペースが挿入されます。

この解決方法は、上記のチャット機能の方法で挙げた二つ目の問題も同時に解決します。つまり、コピー&ペーストする際に、クリップボードには私用領域の文字を表すコードポイントも含まれるので、特別な対応なく埋め込み要素のコピー&ペーストにも対応できます。私たちは、対応するコードポイントを埋め込み要素に置き換えて表示する overlay だけを実装すればよいのです。

もっとも、この方法は埋め込み要素の大きさが既知で、予めフォントを作成できる場合のみ使えます。そのため、私たちは複数の空白サイズのパターンを持つフォントを用意することで解決することも考えています。フォント使用時には埋め込みたい要素のサイズにあったコードポイントを挿入することになります。この場合、不要なパターンを持つことによるファイルサイズの肥大化が心配ですが、今回のように空白文字のみを持つフォントの場合は許容範囲内のサイズ (≈10kB) に収まることを確認しています。

Variable font を使ったより汎用的な空白フォント

上記の Web フォントをより汎用的なものとする Variable font について紹介します。

Variable font とは、フォントフォーマットの OpenType で定められた新しい仕様です。フォントの内部で予め設定した「バリエーション軸」という値に応じて、簡単にフォントを変形させることができます。あまり知られていませんが、モダンブラウザではかなり以前から対応が完了しておりfont-variation-settings という CSS パラメーターで制御できます。

https://v-fonts.com/fonts/ibm-plex-sans-variable

そして、今回の目的でもある「任意の大きさに変形できる空白の文字」を持つフォント「Adobe Blank VF」も既に公開されています。若干フォントの内部設定の変更を要したものの、このフォントを使うことで自由に大きさを制御できる空白文字を得ることができました。

具体的には、以下のような font-face を用意することでコードポイントと空白の大きさをマッピングします。

@font-face {
  font-family: IjdOzq7XxA7DCfZi; /* 一意なフォント名 */
  src: url('./adobe-blank-vf.woff2') format('woff2-variations');
  unicode-range: U+E000; /* 他に使われていないコードポイント */
  font-variation-settings:
    'wdth' 500; /* 空白文字の幅 */
}

そして、textarea の font-family に設定したフォント名を追加することで、指定したコードポイントがそのまま空白文字に置き換わります。空白文字のパターンを増やすことによるファイルサイズの肥大化といった問題も、これにより解決できました。

まとめ

読み込み速度や FOIC/FOUC の問題など、なにかと忌避されがちな Web フォントですが、こういった意外な活用方法を見つけられたのはうれしいです。今後より汎用的に活用できそうなユースケースや API 設計ができたら、OSS 化できると夢が広がりそうです。最後に、contenteditable を使い始める際は本当に必要かよく考えてみることをおすすめします!

UIT 新春 Tech blog記事一覧

  1. Web フォントを使って contenteditable から脱出する
  2. 業務で見つけた! Conditional Types
  3. 業務で役に立つVS Code機能拡張を作ってみた話
  4. フロントエンド開発の業務を支えるちょっと珍しいチームのご紹介
  5. 社内のデザイナーの業務をサポートする LDSG Figma Plugin の工夫したところ、ハマりどころ
  6. 2022年におけるフロントエンド開発のベースライン
  7. Prettier への支援開始のお知らせと企業が OSS に対して支援するということ
  8. 続 LINE 社内用 NPMパッケージの管理戦略