クォートブロックへのペースト処理を修正:`QuoteNodeInserter` の削除でデフォルト挙動に委ねる
クォートブロックへのペースト時に発生していた「カーソル位置の無視」「エディタの応答不能」「余分な空行の挿入」という3つのバグを、カスタム実装の削除というシンプルな方法で解消しました。
背景
クォートブロックへのペーストには、互いに関連する3つのバグが存在していました。いずれも QuoteNodeInserter というカスタム実装が根本原因でした。
具体的には、以下の問題が報告されていました:
- カーソル位置の無視: ペーストしたテキストがカーソル位置ではなくクォートの末尾に挿入される
- エディタの応答不能: 空のクォートブロックにHTMLをペーストすると壊れた構造が生成され、エディタが操作不能になる
- 先頭への空行挿入: ペーストコンテンツの前に余分な空行が挿入される
これらはすべて、QuoteNodeInserter.insertNodes が selection.focus.getNode() に対して insertAfter を呼び出していたことに起因します。この方法ではテキストノード内のカーソルオフセットが考慮されず、フォーカスノードの後ろに無条件で挿入されていました。
技術的な変更
修正内容は src/editor/contents/node_inserter.js から QuoteNodeInserter クラス全体を削除するというものです。追加コードはゼロで、削除のみの変更です。
削除されたクラス:
// Lexical will split a QuoteNode when inserting other Elements - we want them simply inserted as-is
class QuoteNodeInserter extends NodeInserter {
static handles(selection) {
return $getNearestNodeOfType(selection.anchor?.getNode(), QuoteNode)
}
insertNodes(nodes) {
if (!this.selection.isCollapsed()) { this.selection.removeText() }
$ensureForwardRangeSelection(this.selection)
let lastNode = this.selection.focus.getNode()
for (const node of nodes) {
lastNode = lastNode.insertAfter(node)
}
lastNode.selectEnd()
}
}
NodeInserter.for() のインサーター選択リストからも QuoteNodeInserter のエントリが除去され、クォートブロック内でのペーストはLexicalのデフォルト処理に委ねられます。あわせて QuoteNode のimportも不要になったため削除されています。
また、test/browser/tests/paste/paste_into_quote.test.js が新規追加されています。テストはPlaywrightを使ったブラウザテストで、カーソル位置へのテキスト挿入・HTMLペースト時のエディタ応答確認を実機相当の環境で検証します。
// Place cursor after "Hello" using Home then 5x ArrowRight
await editor.send("Home")
for (let i = 0; i < 5; i++) await editor.send("ArrowRight")
await editor.flush()
await editor.paste("INSERTED", { html: "<span>INSERTED</span>" })
await editor.flush()
// The pasted text should appear at cursor position, not at the end
await assertEditorHtml(editor, "<blockquote><p>HelloINSERTED World</p></blockquote>")
テストは Home キーと ArrowRight でカーソルを文字単位で移動させ、ペースト後のHTML構造を直接アサートすることで、カーソル位置の精度を実証しています。
設計判断
カスタム実装を削除してLexicalのデフォルト挙動に委ねるという方針が採用されました。
PRの説明によると、当初は $splitAtPointCaret を使った手動修正も検討されました。しかし最終的にはカスタムハンドラ自体を除去することで全テストが通過したため、Lexicalの標準実装に処理を委ねる方向が選ばれています。QuoteNodeInserter はもともと「LexicalがQuoteNodeを挿入時に分割してしまう」問題への対処として実装されたものでしたが、その実装自体がカーソルオフセットを無視するという新たな問題を生んでいました。
カスタム処理の削除はコードの複雑性を減らし、Lexical本体のバグ修正や改善の恩恵を自動的に受けられる状態に戻すという点でも合理的な判断です。
まとめ
QuoteNodeInserter という21行のカスタム実装を削除することで、3つの連鎖するバグが一度に解消されました。「フレームワークのデフォルト挙動を上書きするコードが、そのフレームワーク本来の能力(カーソルオフセットの考慮)を損なっていた」という構図は、カスタム処理の必要性を定期的に見直すことの重要性を示しています。