インラインコードからのキャレット脱出時にフォーマットが引き継がれるバグを修正
<code> スパンの末尾で ArrowRight キーを押してキャレットを外に移動させた後、次に入力した文字がコードフォーマットのまま挿入される問題を修正しました。KEY_ARROW_RIGHT_COMMAND ハンドラを追加し、キャレットが <code> ノードを抜けるタイミングで明示的にフォーマットを解除することで解決しています。
背景
Lexical エディタでは、インラインフォーマット(code など)はキャレットが持つアクティブフォーマット情報として管理されています。通常、<code> 要素の外に移動すればフォーマットは自動的にクリアされることが期待されますが、ArrowRight によるキャレット移動ではこのクリアが行われないケースがありました。
具体的には、<code> ノードの末尾にキャレットが置かれた状態で ArrowRight を押すと、キャレットは視覚的に <code> の外へ移動します。しかし、アクティブなインラインフォーマットの状態は code のままとなり、その直後に文字を入力すると意図せずコードフォーマットで挿入されていました。
技術的な変更
CommandDispatcher クラスに KEY_ARROW_RIGHT_COMMAND のハンドラを新規追加することで、ArrowRight キー操作時のフォーマット状態を明示的に制御するようにしました。
変更前:
#registerKeyboardCommands() {
this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleTabKey.bind(this), COMMAND_PRIORITY_NORMAL)
}
変更後:
#registerKeyboardCommands() {
this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#handleArrowRightKey.bind(this), COMMAND_PRIORITY_NORMAL)
this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleTabKey.bind(this), COMMAND_PRIORITY_NORMAL)
}
#handleArrowRightKey(event) {
const selection = $getSelection()
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
if (this.selection.isInsideCodeBlock || !selection.hasFormat("code")) return false
const anchorNode = selection.anchor.getNode()
if (!$isTextNode(anchorNode) || selection.anchor.offset !== anchorNode.getTextContentSize()) return false
if (this.selection.nodeAfterCursor !== null) return false
event.preventDefault()
selection.toggleFormat("code")
return true
}
ハンドラが実際に介入する条件は複数のガード節で厳密に絞られています。具体的には以下のすべてを満たす場合にのみフォーマット解除を実行します:
- 選択がコラプス済み(キャレット状態)であること
- コードブロック内ではなくインラインコードであること
- 現在のフォーマットに
codeが含まれていること - アンカーノードがテキストノードであり、オフセットがノードの末尾と一致すること
- キャレット直後に続く同一フォーマットのノードが存在しないこと(
nodeAfterCursor === null)
これらの条件が揃ったとき、event.preventDefault() でデフォルトのキャレット移動を抑制した上で selection.toggleFormat("code") を呼び出してフォーマットをオフにし、return true でコマンドの処理済みを宣言します。
設計判断
フォーマット解除をキャレット移動に割り込む形で実装する方式が選ばれました。
Lexical のコマンドシステムでは、ハンドラが true を返すとそのコマンドの処理が止まり、以降の低優先度ハンドラやデフォルト動作が呼ばれなくなります。このPRでは COMMAND_PRIORITY_NORMAL でハンドラを登録し、event.preventDefault() と return true の組み合わせによってキャレット移動そのものをカスタムロジックに置き換えています。これにより、フォーマット状態の変更とキャレット移動を一つのトランザクションとして一貫して処理できます。
また、isInsideCodeBlock による早期リターンも注目に値します。コードブロック(<pre><code>)内では同名の code フォーマットが使われますが、その文脈での ArrowRight は通常のテキスト移動として扱うべきです。このガード節によって、コードブロック内の動作には一切影響を与えないよう配慮されています。
付随する削除:dropFiles メソッドの廃止
このPRには、インラインコードの修正とは独立した dropFiles メソッドの削除も含まれています。Contents クラスから dropFiles メソッドが削除され、ドラッグ&ドロップ時のファイルアップロードは直接 uploadFiles を呼ぶように変更されました。
削除されたのは、OS のファイルマネージャーからのドラッグ中にブラウザが DOM のセレクションを更新しないため、caretRangeFromPoint / caretPositionFromPoint を用いてドロップ座標にキャレットを移動させる #moveSelectionToPoint メソッドと、それを呼び出していた dropFiles メソッドです。これらが削除されたことで、ドロップ位置へのキャレット移動ではなく、selectLast: true オプションによる最後にアップロードされたファイルの選択という動作に統一されています。
まとめ
本PRは、Lexical のコマンドシステムに ArrowRight ハンドラを挿入することで、インラインコードからのキャレット脱出時のフォーマット残留というエッジケースを精確に修正しています。複数のガード条件を組み合わせてコードブロックや範囲選択などの正常ケースを除外した上でのみ介入する設計は、既存の動作への影響を最小限に抑えながら問題を解消する手堅いアプローチです。