blockquote内へのペーストで分割が発生するバグを修正
QuoteNode 内にHTMLをペーストすると、見出しやコードブロックなどのブロック要素がblockquoteの外側に分割されて挿入される問題を修正しました。NodeInserters が selection.insertNodes() ではなく専用の挿入パスを使うことで、ペースト先のノードが分割されなくなります。
背景
Lexicalの selection.insertNodes() は、ブロック要素を含むノード群を挿入する際に、現在のカーソル位置を起点にして受け取り側のノードを分割する挙動を持ちます。通常の段落では期待通りに動作しますが、QuoteNode 内にカーソルがある状態でヘッダー(<h1>)やコードブロック(<pre>)などのブロック要素をペーストすると、blockquoteが途中で分割され、一部のノードがquoteの外側に吐き出される問題が起きていました。
Fizzy card #4979 として報告されていたこの問題は、「引用テキストとしてのペーストが期待通りに動作しない」という形でユーザーに影響していました。
技術的な変更
insertDOM メソッドにおける挿入処理が、selection.insertNodes() の直接呼び出しから insertAtCursor() 経由に統一されました。これにより、QuoteNode 内へのペーストでは分割ではなくノードの単純な挿入が行われるようになります。
変更前:
insertDOM(doc, { tag } = {}) {
this.#unwrapPlaceholderAnchors(doc)
if (tag === PASTE_TAG) this.#stripTableCellColorStyles(doc)
this.editor.update(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) return
const nodes = $generateNodesFromDOM(this.editor, doc)
if (!this.#insertUploadNodes(nodes)) {
selection.insertNodes(nodes)
}
}, { tag })
}
変更後:
insertDOM(doc, { tag } = {}) {
this.#unwrapPlaceholderAnchors(doc)
this.editor.update(() => {
if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc)
const nodes = $generateNodesFromDOM(this.editor, doc)
if (!this.#insertUploadNodes(nodes)) {
this.insertAtCursor(...nodes)
}
}, { tag })
}
変更に伴い、いくつかの重要な修正が加えられています。まず、$hasUpdateTag(PASTE_TAG) による判定がエディタの update() コールバック内に移動しています。これは tag オプションがコールバック内でのみ有効なため、より正確な判定が可能になります。また、$isRangeSelection(selection) による早期リターンが削除され、選択状態のチェックは insertAtCursor() の責務に委譲されています。
uploader.js でも同様に、forEach による個別呼び出しからスプレッド構文による一括渡しに統一されています。
変更前:
$insertUploadNodes() {
this.nodes.forEach(this.contents.insertAtCursor)
}
変更後:
$insertUploadNodes() {
this.contents.insertAtCursor(...this.nodes)
}
テストでは、<h1>・<pre><code>・<p> を含むHTMLをblockquote内にペーストした際、すべての要素がblockquote内に留まり、外側に <p>・<h1>・<pre> が1つも出力されないことを検証するケースが追加されました。
設計判断
selection.insertNodes() を迂回して insertAtCursor() に統一するアプローチが採用されました。Lexicalの insertNodes() はノードを分割して挿入する汎用的なAPIですが、QuoteNode のような「内容をまとめて保持する」コンテナノードとの相性が悪いケースがあります。insertAtCursor() は内部的に $getNearestNodeOfType や $ensureForwardRangeSelection を活用し、挿入先のコンテキストに応じた振る舞いを実現できます(diffに $getNearestNodeOfType と $ensureForwardRangeSelection の新規インポートが追加されていることからも、この方向性が読み取れます)。
また、PASTE_TAG の判定をコールバック外からコールバック内の $hasUpdateTag() に移したことで、Lexicalのupdateタグの仕組みと整合した、より正確な状態チェックが実現されています。
まとめ
この修正は、Lexicalの汎用的な insertNodes() APIの分割挙動が QuoteNode という特定のコンテキストで引き起こす問題を、挿入ロジックの統一によって解消しています。selection.insertNodes() の直接呼び出しをコードベースから排除することで、ペースト処理の経路が一本化され、コンテナノードの種類によらず一貫した挿入動作が保証されるようになりました。