Firefox でのカット&ペースト二重挿入バグを修正

basecamp/lexxy

Firefox / Gecko 系ブラウザでカット&ペーストを行うと、コンテンツが二重に挿入されるバグが修正されました。リッチテキストのペースト処理パスで event.preventDefault() が呼ばれていなかったことが原因です。

背景

#793 として報告されたこのバグは、Firefox でのみ再現する挙動でした。テキストを入力し、切り取り(カット)してから貼り付け(ペースト)すると、同じテキストが2回挿入されるという問題です。

Firefox と Chrome では、clipboardEvent.preventDefault() を呼ばなかった場合の挙動が異なります。Chrome はカスタムハンドラが DOM を書き換えた後にネイティブのペースト処理を無視しますが、Firefox はカスタムハンドラと並行してネイティブのペースト処理も DOM を変更します。Lexical の Mutation Observer がこの「ネイティブによる DOM 変更」を新たな入力として検知し、コンテンツを二重に処理していました。

プレーンテキストや URL のペーストパスではすでに preventDefault() が呼ばれており、この問題は発生していませんでした。リッチテキスト・ファイルを処理する #handlePastedFiles パスだけが対応漏れの状態でした。

技術的な変更

修正は src/editor/clipboard.js に集中しており、3つの独立した変更から構成されています。

変更1: #handlePastedFiles の戻り値を使った preventDefault の呼び出し

これまで #handlePastedFiles の戻り値は呼び出し元にそのまま返すだけでしたが、true を返した場合(つまりカスタムハンドラが処理を行った場合)に限り event.preventDefault() を呼ぶよう変更されました。

変更前:

return this.#handlePastedFiles(clipboardData)

変更後:

const handled = this.#handlePastedFiles(clipboardData)
if (handled) event.preventDefault()
return handled

変更2: Lexical 自身のクリップボードデータを除外

#handlePastedFiles 内の HTML 処理パスに、#isLexicalClipboardData によるガード条件が追加されました。clipboardData.types"application/x-lexical-editor" が含まれている場合は HTML の挿入をスキップします。これは Lexical が内部的に使用するクリップボード形式を誤って処理しないための安全策です。

if (html && !this.#isLexicalClipboardData(clipboardData)) {
  this.contents.insertHtml(html, { tag: PASTE_TAG })
  return true
}

変更3: ファイルが存在しない場合に false を返す

これまで #handlePastedFilesfiles の有無に関わらず return true で終了していました。ファイルが空の場合は false を返すよう修正され、「処理した」というシグナルを正確に伝えるようになっています。

if (files.length) {
  this.#uploadFilesPreservingScroll(files)
  return true
}

return false

テスト側では test/browser/tests/paste/paste.test.js に「カット&ペーストでコンテンツが重複しない」ことを検証するブラウザテストが追加され、assertEditorHtml を使って最終的な DOM 状態を確認しています。

設計判断

「ハンドラが処理した場合のみ preventDefault を呼ぶ」 という設計は、Lexical 本体の onPasteForRichText ハンドラと同じ方針です。カスタムハンドラが処理しなかったケースでブラウザのデフォルト動作を妨げないという一貫性が保たれています。

#isLexicalClipboardData を別メソッドとして切り出した点も注目に値します。Lexical 内部のクリップボード形式を判定するロジックが名前付きメソッドとして明示されることで、将来的な条件追加やテストが書きやすくなっています。また、#handlePastedFiles が「何も処理しなかった」場合に明示的に false を返すようにしたことで、戻り値のセマンティクスが正確になり、呼び出し元の if (handled) event.preventDefault() が正しく機能します。

まとめ

この修正は、ブラウザ間のイベント処理の差異を突いたバグを、preventDefault() の呼び出し漏れという単純な原因に帰着させた好例です。戻り値のセマンティクスを厳密にしつつ、Lexical 本体のハンドラと一貫した設計に揃えることで、同種のブラウザ差異バグへの耐性も高めています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
2cd9d097

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)」という3部構成が明確に守られており、非常に分かりやすい記事構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```javascript:filepath)およびGitHubのPR・Issueへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

専門用語が適切に使用されており、リッチテキストエディタ開発に関わるエンジニアという対象読者に適した技術レベルで書かれています。

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

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

各セクション・各パラグラフが「総論→各論」の構成になっており、トピックセンテンスが明確です。1段落1トピックの原則も守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiffの内容と完全に一致しており、変更点が正確に表現されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Mutation Observer」、「clipboardData.types」などの技術用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

FirefoxとChromeにおけるイベント処理の挙動の違いや、それによるバグの発生メカニズムが、PR情報とDiffに基づいて技術的に正確かつ論理的に説明されています。

事実の突合 ✓ PASS

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

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

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

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

PR番号(#868)やIssue番号(#793)などの固有名詞・数値が正確に記載されています。

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

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

記事のタイトル「Firefox でのカット&ペースト二重挿入バグを修正」は、元のPRの主題を的確に要約しています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれていない、バージョンサポート状況やリリース日程といった外部知識の追記はなく、事実に基づいた記述が徹底されています。

時間表現の正確性 ✓ PASS

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

「すでに...呼ばれており」といった時間表現が、PR Descriptionの「already called」と一致しており、時間的な前後関係が正確です。