テキストドラッグ時のクラッシュをオプショナルチェーンで修正
テキストノードに対して .closest() を呼び出していたバグを、オプショナルチェーン(?.)の追加で修正しました。これにより、テキストのドラッグ操作後にコードフォーマットを適用するとエディタがクラッシュする問題が解消されます。
背景
ブラウザでテキストを選択してドラッグすると、dragstart イベントの event.target がテキストノード(Text)になる場合があります。テキストノードは Element インターフェースを持たないため、.closest() メソッドが存在せず、呼び出すと TypeError: event.target.closest is not a function が発生します。
アタッチメントのドラッグハンドラ(AttachmentDragAndDrop)は、#handleDragStart 内で event.target.closest("textarea") および event.target.closest("figure.attachment[...]")を無条件に呼び出していました。テキストのドラッグ操作によってこのパスが踏まれると例外が発生し、その後のコードフォーマット適用など、エディタの操作全体に支障をきたしていました。
なお、PRの説明によれば、以前の対処として DRAGSTART_COMMAND をインターセプトしてネイティブのテキストD&Dをすべてブロックする方法が試みられていましたが、これはテキストのドラッグ&ドロップ自体を完全に無効化してしまうため、今回の修正で取り除かれています。
技術的な変更
src/editor/attachments/drag_and_drop.js の #handleDragStart メソッドに対し、2箇所の .closest() 呼び出しにオプショナルチェーン(?.)を追加しました。
変更前:
#handleDragStart(event) {
if (event.target.closest("textarea")) return false
const figure = event.target.closest("figure.attachment[data-lexical-node-key]")
if (!figure) return false
this.#draggedNodeKey = figure.dataset.lexicalNodeKey
変更後:
#handleDragStart(event) {
if (event.target.closest?.("textarea")) return false
const figure = event.target.closest?.("figure.attachment[data-lexical-node-key]")
if (!figure) return false
this.#draggedNodeKey = figure.dataset.lexicalNodeKey
?. を付けることで、event.target が .closest を持たない場合(テキストノード等)はメソッド呼び出しをスキップして undefined を返します。undefined はfalsyであるため、if (!figure) return false の分岐でハンドラが早期リターンし、例外なく処理が終わります。
また、test/browser/tests/formatting/drag_and_drop_formatting.test.js が新たに追加され、Playwrightを使ったブラウザテストでこの修正を検証しています。テストはテキストを選択してマウスAPIでドラッグ操作を行い、その後「Code」ボタンでコードフォーマットを適用するシナリオを再現します。page.on("pageerror", ...) でページエラーを収集し、closest is not a function や node not found を含むエラーが発生しないことをアサートしています。
設計判断
?. によるガードをハンドラ内に局所化する方針が採用されました。
event.target の型チェック(instanceof Element)を先行させる方法も考えられますが、オプショナルチェーンはより短く、.closest() の呼び出し箇所に意図を直接表現できます。また、event.target が Element でない場合に closest が undefined になるという自然な挙動を利用しており、余分な早期リターン条件の追加を避けています。
代替案として試みられた DRAGSTART_COMMAND のインターセプトは、ネイティブのテキストD&Dを全面的に無効化するものであり、機能そのものを損なうトレードオフが大きいと判断されました。今回の修正はアタッチメントドラッグハンドラの責務を変えず、テキストノードという想定外の入力に対してのみ安全にスキップする最小限の変更に留まっています。
まとめ
2文字の ?. を追加するだけで、テキストノードという非Element対象への防御が完結しています。ネイティブのテキストD&Dを温存しながらアタッチメントハンドラの堅牢性を高めたこの修正は、最小限の変更で最大の互換性を保つという点で、バグ修正の手本となるアプローチです。