ブロッククォート内へのペーストで改行後のテキストが外に逃げる問題を修正
ブロッククォート内に改行を含むプレーンテキストをペーストすると、最初の改行以降のテキストがクォートの外へ抜け出てしまう不具合を修正しました。Lexicalの selection.insertNodes() がデフォルトで ParagraphNode を QuoteNode の外へ分離する挙動を回避する専用の挿入パスを追加することで解決しています。
背景
Lexicalの selection.insertNodes() は、ペーストされたノードを現在の選択位置に挿入する汎用メソッドですが、QuoteNode の内部に ParagraphNode を挿入しようとした場合、そのパラグラフをクォートの外側に押し出す(split out)動作をデフォルトで行います。この挙動はエディタの一般的なコンテキストでは自然ですが、ブロッククォート内にテキストをペーストする場面では意図に反します。
具体的には、"line one\nline two" のようなテキストをペーストすると、Lexicalは "line one" と "line two" をそれぞれ別の ParagraphNode として生成します。insertNodes() がこれらをクォート内に挿入しようとした際、2番目以降の ParagraphNode がクォート外のトップレベルに配置されてしまい、結果として "line two" がブロッククォートから脱出する形になっていました。
技術的な変更
src/editor/contents.js に、選択位置が QuoteNode の内部かどうかを検出し、該当する場合は専用の挿入処理に切り替えるパスが追加されました。
変更前は、ペーストされたノードを常に insertNodes() で挿入していました。
変更前:
const nodes = $generateNodesFromDOM(this.editor, doc)
if (!this.#insertUploadNodes(nodes)) {
selection.insertNodes(nodes)
}
変更後:
const nodes = $generateNodesFromDOM(this.editor, doc)
if (!this.#insertUploadNodes(nodes)) {
if (this.#isInsideQuoteNode(selection)) {
this.#insertNodesIntoQuote(selection, nodes)
} else {
selection.insertNodes(nodes)
}
}
追加されたプライベートメソッド #isInsideQuoteNode(selection) は、アンカーノード自身が QuoteNode であるか、または getTopLevelElement() が QuoteNode であるかを判定します。これにより、クォート内の任意の深さの位置からのペーストを正しく検出できます。
#insertNodesIntoQuote(selection, nodes) は、生成されたノードを QuoteNode の子として直接追加する処理を担います。このメソッドのロジックは以下のように動作します。
- 現在のカーソル位置の
ParagraphNode(currentParagraph)を特定する - ペーストされたノードを順番に処理し、
insertAfterを使ってクォート内に追加していく -
ParagraphNodeの場合:現在のパラグラフが空であれば、その子要素を移植して置き換える。空でなければinsertAfterで後続に追加する -
ParagraphNode以外のノードの場合:新しいParagraphNodeでラップしてから追加する
この設計により、"line one\nline two" のような単一改行(<br> として処理)、複数行、そして "\n\n" による段落分割(<p> 要素として分割)のいずれのケースも、ブロッククォート内に正しく収まります。
テストは test/browser/tests/paste/paste.test.js にブラウザ統合テストとして追加されており、単一改行・複数行・段落区切りの3パターンが assertEditorHtml() によって期待するHTMLと照合されます。
設計判断
Lexicalの insertNodes() の挙動を修正するのではなく、クォートコンテキストを検出して別の挿入パスに分岐するアプローチが採用されています。
この判断の背景には、insertNodes() がLexicalのコアAPIであり、その汎用的な挙動を変えることがブロッククォート以外の多くのケースに影響を及ぼすリスクがある点が挙げられます。代わりに呼び出し側で文脈を判別し、クォート専用のロジックを切り出すことで、既存の挿入処理に対する影響範囲を最小化しています。
空のパラグラフに対して「子要素を移植して置き換える」処理を特別扱いしている点も注目に値します。クォートブロックを挿入した直後はパラグラフが空の状態であることが多く、ペーストによって余分な空パラグラフが残らないよう配慮されています。
まとめ
本PRは、LexicalのAPIの既定挙動とブロッククォートのセマンティクスとの摩擦に対し、呼び出し側でコンテキストを判別する専用パスを追加することで対処しました。insertNodes() への依存を維持しつつ、クォート内ペーストという特定のシナリオだけを切り離して丁寧にハンドリングする設計は、エディタのノードモデルを扱う上での実践的なパターンといえます。