テキストドラッグ時のクラッシュをオプショナルチェーンで修正

basecamp/lexxy

テキストノードに対して .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 functionnode not found を含むエラーが発生しないことをアサートしています。

設計判断

?. によるガードをハンドラ内に局所化する方針が採用されました。

event.target の型チェック(instanceof Element)を先行させる方法も考えられますが、オプショナルチェーンはより短く、.closest() の呼び出し箇所に意図を直接表現できます。また、event.targetElement でない場合に closestundefined になるという自然な挙動を利用しており、余分な早期リターン条件の追加を避けています。

代替案として試みられた DRAGSTART_COMMAND のインターセプトは、ネイティブのテキストD&Dを全面的に無効化するものであり、機能そのものを損なうトレードオフが大きいと判断されました。今回の修正はアタッチメントドラッグハンドラの責務を変えず、テキストノードという想定外の入力に対してのみ安全にスキップする最小限の変更に留まっています。

まとめ

2文字の ?. を追加するだけで、テキストノードという非Element対象への防御が完結しています。ネイティブのテキストD&Dを温存しながらアタッチメントハンドラの堅牢性を高めたこの修正は、最小限の変更で最大の互換性を保つという点で、バグ修正の手本となるアプローチです。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6928d106

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術詳細・設計判断(各論)、まとめ(結論)の3部構成が明確に守られており、非常に分かりやすい構成です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名指定は正しく使われています。しかし、PRへのリンクが`[PR #969](URL)`となっており、ガイドラインで推奨される`[#969](URL)`形式と異なります。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「テキストノード」「.closest()」「オプショナルチェーン」といった技術用語を前提としており、専門知識を持つエンジニアという対象読者に適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが「総論→各論」の構造で書かれており、見出しと各段落の1文目だけで要点を追うことができます。1段落1トピックも守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内の「変更前」「変更後」のコードブロックは、提供されたDiffの内容と完全に一致しています。ファイルパスの記載も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「オプショナルチェーン」「テキストノード」「TypeError」など、PR descriptionと一致した正確な技術用語が使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

オプショナルチェーンがなぜこの問題の解決策になるのか(`undefined`を返し、falsyな値として評価される)という技術的な説明が、正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescriptionやDiff内容によって裏付けられており、ハルシネーション(捏造)は一切見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号「#969」が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトル「テキストドラッグ時のクラッシュをオプショナルチェーンで修正」は、PRの主題「Fix text drag crash in attachment drag handler」を正確に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれないバージョンサポート状況やリリース日程などの外部知識は含まれておらず、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「以前の対処」「今回の修正」といった時間表現は、PRの文脈と正確に一致しています。