空ノード選択時のアップロードクラッシュを防止
Lexxy のドキュメントレベルのファイルドロップが、削除済みノードが残す 空の NodeSelection によりクラッシュしていた問題を、選択状態のフォールバックと Inserter の判定を改良することで解消しました。
背景
この PR は Sentry の BC3-JS-N7D5 (Issue #7507327343) に起因するクラッシュを対象としています。ドロップゾーンがエディタにファイルを転送する際、直前に選択されていたノードが削除されると、エディタは ノード選択が存在しない状態 を保持し、Contents#uploadFiles が例外を投げていました。
根本原因は Contents#insertAtCursor が $getSelection() が null の場合のみ文書末へフォールバックしていた点です。空の NodeSelection は null ではなく、取得できるノードが0 個というだけであり、このケースはフォールバック対象に含まれていませんでした。そのため、空選択がそのまま NodeInserter に渡されました。
NodeSelectionNodeInserter.insertNodes は選択されたノードの最後要素 (this.selection.getNodes().at(-1)) を取得し、lastNode.insertAfter(...) を実行します。空のノード配列では lastNode が undefined となり、is や insertAfter の呼び出しで "Cannot read properties of undefined" エラーが発生していました。
技術的な変更
src/editor/contents.js に $isNodeSelection のインポートを追加し、プライベートメソッド #insertableSelection() を導入しました。このメソッドは現在の選択を取得し、空の NodeSelection であれば文書末 ($getRoot().selectEnd()) を返し、そうでなければ元の選択または null の場合は同様に文書末へフォールバックします。
@@ -1,6 +1,6 @@
import {
$createLineBreakNode, $createParagraphNode, $createTextNode, $getNodeByKey, $getRoot, $getSelection, $hasUpdateTag,
- $isLineBreakNode, $isParagraphNode, $isRangeSelection, $isRootOrShadowRoot, $isTextNode, $setSelection,
+ $isLineBreakNode, $isNodeSelection, $isParagraphNode, $isRangeSelection, $isRootOrShadowRoot, $isTextNode, $setSelection,
HISTORY_MERGE_TAG, PASTE_TAG,
SELECTION_INSERT_CLIPBOARD_NODES_COMMAND
} from "lexical"
@@
insertAtCursor(...nodes) {
- const selection = $getSelection() ?? $getRoot().selectEnd()
+ const selection = this.#insertableSelection()
const inserter = NodeInserter.for(selection)
inserter.insertNodes(nodes)
@@
#insertableSelection() {
- const selection = $getSelection()
- if ($isNodeSelection(selection) && selection.getNodes().length === 0) {
- return $getRoot().selectEnd()
- }
-
- return selection ?? $getRoot().selectEnd()
+ const selection = $getSelection()
+ if ($isNodeSelection(selection) && selection.getNodes().length === 0) {
+ return $getRoot().selectEnd()
+ }
+
+ return selection ?? $getRoot().selectEnd()
}
src/editor/contents/node_inserter/node_selection_node_inserter.js では、ハンドラ判定を厳密化し、空のノード選択を扱わないようにしました。
@@ -4,7 +4,7 @@ import BaseNodeInserter from "./base_node_inserter"
export default class NodeSelectionNodeInserter extends BaseNodeInserter {
static handles(selection) {
- return $isNodeSelection(selection)
+ return $isNodeSelection(selection) && selection.getNodes().length > 0
}
テスト upload_into_unsupported_selection.test.js に空ノード選択からのファイルアップロードケースを追加し、エラーが発生しないことと、添付がドキュメント末尾に正しく挿入されることを検証しています。
設計判断
Contents#insertAtCursor が空のノード選択を 欠損選択と同等に扱う 方針は、ドキュメント全体で統一されたフォールバックロジックを提供します。これにより、ドロップ、貼り付け、ツールバーからの挿入といった全エントリポイントが同一の挙動を保証し、個別のガードを書き散らす必要がなくなります。
NodeSelectionNodeInserter.handles に ノード数 > 0 の条件を加えることで、Inserter 自体が不正な状態に入らないことを保証しています。判定ロジックが Inserter の責務に収まることで、Contents 側のロジックはシンプルに保たれ、将来的な Inserter の拡張が容易になります。
この変更は STYLE.md の fail‑fast ガイドラインに沿っており、空のノード選択はオプション状態 とみなして早期に安全なデフォルトへ遷移させる設計です。未サポート状態を例外で隠すのではなく、自然に文書末への挿入へ導くことでユーザー体験が向上します。