Firefox でのカット&ペースト二重挿入バグを修正
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 を返す
これまで #handlePastedFiles は files の有無に関わらず 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 本体のハンドラと一貫した設計に揃えることで、同種のブラウザ差異バグへの耐性も高めています。