EditContext API で自作エディタを作ろう!!

自己紹介

丹野 翔太(tanshio)

  • 株式会社FRAIM / 株式会社SALT
  • フロントエンドエンジニア
  • 最近の所感:git worktree + AIに疲れた
  • Twitter: @_tanshio

エディタを作りたい

エディタのライブラリ紹介

Slate

React フレンドリー。ノードモデルを自分で設計しやすい。

柔軟だけど、IME / selection / normalize の設計力が必要。

ProseMirror / Tiptap

拡張エコシステムが強い。実戦投入しやすい。

ドキュメントモデルと plugin 理解の学習コストは高め。

Lexical

Meta 製。更新モデルとパフォーマンス設計がモダン。

API の作法に乗ると強い。内部設計の理解が効く。

共通してやっていること

  • 内部の文書モデルを持つ
  • DOM 選択範囲と文書位置を相互変換する
  • IME / clipboard / composition を吸収する

カスタマイズ

複雑なことをやろうとすると大変

今ならAIでゴリ押しできそうではある

contenteditable のつらみ

DOM が真実になりがち

  • ブラウザごとの差分が出る
  • 予期しない DOM 変形が起きる
  • 整形・正規化の責務が重い

IME / selection が難所

  • composition 中に state 更新で壊れやすい
  • Range と内部モデルのずれ
  • 日本語入力での再現確認コスト
  • これもブラウザ間で違う

見た目を作り込みにくい

  • DOM を直接編集させる都合で制約が多い
  • カスタム caret / selection と競合しやすい

要するに

入力と描画が密結合なのがしんどい。

「入力はネイティブ」「描画は自前」に分けたい。

EditContext の紹介(APIの使い方)

contenteditable のつらみの解消を狙う API

基本フロー(最小構成)

  1. const ec = new EditContext()
  2. フォーカス可能な要素に element.editContext = ec
  3. textupdate を購読して文字列を更新
  4. 自前 UI(テキスト/選択/キャレット)を再描画

ポイント: DOM を直接編集させずに、イベント駆動で状態を更新する。

アプリ側で持つもの

  • 文書モデル(今回の例では `text + marks`)
  • 選択範囲(開始/終了)
  • 表示ロジック(段落、装飾、カスタム UI)
  • Undo / Redo などの編集履歴

EditContext = エディタそのものではない
あくまで「入力コンテキスト」を渡してくれる API。

EditContext の紹介(イベントと状態)

状態(読むもの)

  • ec.text
  • ec.selectionStart
  • ec.selectionEnd
  • ec.characterBoundsRangeStart(実装依存)

主要イベント

  • textupdate: テキスト更新
  • compositionstart / compositionend
  • textformatupdate: IME変換中の装飾情報
  • characterboundsupdate: 文字矩形の問い合わせ

設計の勘所

「入力イベントを受ける層」と「描画する層」を分離する。

React state / internal model / render の責務が整理しやすい。

EditContext API の使いどころ

向いているケース

  • 表示を完全に自前で描きたい(canvas / custom DOM / 仮想化)
  • コードエディタ / リッチテキスト / 図表入りエディタなど、描画要件が強い
  • IME(日本語入力)や OS の入力体験は活かしたい
  • contenteditable の DOM 差分管理から離れたい

あまり向いていないケース(現時点)

  • 対応ブラウザを広く取りたいプロダクション機能(Chromeのみ)
  • まずは早く WYSIWYG を作りたい(Tiptap / Lexical の方が早い)
  • 座標同期や IME 周りの検証コストを取りにくいチーム

なんとVS Codeで使用されている

editor.editContext

デモ

BIUndoRedo
Try EditContext + custom rendering
選択した範囲に装飾メニュー
にほんごへんかんちゅう

「見た目は完全に自前」でも、IME の候補位置・入力は OS と連携できるのが面白い。

デモ(2/2)

今回の Playground でやっていること

div[role=textbox]editContext を付与し、React 側でテキスト・選択・装飾・キャレットを描画。

  • 文字列本体は ec.text / textupdate と同期
  • 選択変更は updateSelection(...) とローカル state を同期
  • Bold / Italic は `mark` 方式の範囲装飾として管理

座標同期まわり(IMEの肝)

  • Range で caret / selection の矩形を計測
  • updateControlBounds / updateSelectionBounds を返す
  • characterboundsupdate に対して updateCharacterBounds
  • 矩形が取れないフレームは再計測(ちらつき対策)

デモで見せたいメッセージ

「入力をネイティブに任せつつ、表示は自前にできる」 を体感してもらう。

その代わり、selection/caret/IME座標の同期責務は増える。

EditContext のつらみ

対応状況が厳しい

  • 実験的 API(ブラウザ/フラグ依存)
  • 本番導入の前提条件を作りづらい

座標同期の責務が重い

  • selectionBounds がズレると IME 表示もズレる
  • Range が取れない瞬間のフォールバック設計が必要

OS/IME UI との共存

  • OS 側キャレットと custom caret の二重表示
  • composition 中の描画ポリシーを決める必要

結局必要なもの

エディタ実装力そのもの

contenteditable の苦労がゼロになるわけではなく、責務の置き場所が変わる。ただ、今はAIがあるのでゴリ押しできそう

おわりに

みんなも自作エディタを作ろう!