テーブルセルへの連続画像アップロード時にサイレントドロップが発生するバグを修正
Lexicalエディタのテーブルセルに対して連続して画像をアップロードすると、2枚目以降の画像が無音で消失するバグが修正されました。根本原因は selectEnd() が シャドウルートノード 上にセレクションを着地させてしまうことで、挿入先の解決に失敗していた点です。
背景
テーブルセルへの1枚目の画像アップロード後、クリック操作なしにすぐ2枚目をアップロードすると、エディタは画像を黙って捨てていました。これはアップロード完了後のカーソル位置の正規化が、テーブル内のシャドウルートに対して適切に行われていなかったことが原因です。
具体的には、1枚目のアップロード完了後に selectEnd() を呼び出すと、セレクションが TableCellNode(Lexicalにおけるシャドウルート)自体の上に落ちます。この状態では2つの問題が生じます。まず、isOnPreviewableImage がシャドウルート上のセレクションを誤判定し、GalleryUploader 経由のフローへルーティングしてしまいます。次に、insertNodes() がシャドウルート内でブロック親ノードを見つけられず、挿入自体が失敗します。
この組み合わせにより、エラーも警告も発生させずに画像が消失するという、デバッグが困難なバグが引き起こされていました。
技術的な変更
insertAtCursor にシャドウルート上のセレクションを正規化するガード処理を追加し、セレクション変数を const から let に変更することで再代入可能にしました。
変更前:
insertAtCursor(node) {
const selection = $getSelection() ?? $getRoot().selectEnd()
const selectedNodes = selection?.getNodes()
if ($isRangeSelection(selection)) {
selection.insertNodes([ node ])
} else if ($isNodeSelection(selection) && selectedNodes.length > 0) {
const lastNode = selectedNodes.at(-1)
lastNode.insertAfter(node)
}
}
変更後:
insertAtCursor(node) {
let selection = $getSelection() ?? $getRoot().selectEnd()
const selectedNodes = selection?.getNodes()
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode()
if ($isShadowRoot(anchorNode)) {
const paragraph = $createParagraphNode()
anchorNode.append(paragraph)
selection = paragraph.selectStart()
}
selection.insertNodes([ node ])
} else if ($isNodeSelection(selection) && selectedNodes.length > 0) {
const lastNode = selectedNodes.at(-1)
lastNode.insertAfter(node)
}
}
セレクションのアンカーノードが $isShadowRoot(TableCellNode など)であると判定された場合、空の $createParagraphNode をそのセルに append し、そのパラグラフ先頭にセレクションを移動してから insertNodes を呼び出します。これにより、insertNodes がブロック親ノードを正しく見つけられる状態が保証されます。
また、insertDOM メソッドからは #unwrapPlaceholderAnchors の呼び出しと、クォートノード内への特殊挿入パス(#isInsideQuoteNode / #insertNodesIntoQuote)が削除されました。これらのコードパスが整理されたことで、selection.insertNodes(nodes) への統一フローが実現しています。インポート宣言も $isElementNode、$isRootNode、$isRootOrShadowRoot などシャドウルート判定に必要なユーティリティを追加する形で更新されています。
テストの追加
バグの再発を防ぐため、テーブルセルへの複数画像アップロードをカバーするブラウザテストが test/browser/tests/tables/structure.test.js に追加されました。
2つのテストケースが新設されています。一方は複数ファイルを同時に選択して一括アップロードするケース、もう一方はファイルを1枚ずつ順番にアップロードするケースです。どちらも mockActiveStorageUploads でネットワーク通信をモックした上で figure.attachment の数が期待通りか検証しており、今回修正されたバグシナリオを直接カバーしています。
設計判断
セレクションの正規化をノード挿入直前に行う方式 が採用されました。uploadFiles 側でアップロード完了後のセレクション管理を変更する代わりに、挿入処理の入口である insertAtCursor でシャドウルート上のセレクションを検出・修正することで、他のコードパスからの呼び出し時にも同様の保護が働きます。
シャドウルート上に段落を append してからセレクションを移す手法は、insertNodes が前提とする「セレクションがブロック親を持つ」という契約を満たすための最小限の介入です。セルが空の場合でも挿入後に不要な段落が残らないよう、Lexicalの通常ノード挿入フローに処理を委ねる設計になっています。
まとめ
この修正は、Lexicalのシャドウルートという概念がもたらす「セレクションが期待外の場所に着地する」問題に対して、挿入処理の入口で防御的に正規化するパターンを確立したものです。insertAtCursor へのガード追加という局所的な変更で、テーブルセルを含むすべての挿入コードパスに対して一貫した保護が実現されています。