Lexicalのトリプルクリックハンドラを上流で無効化し、選択動作を修正
Lexicalのトリプルクリックハンドラが引き起こす不正な選択動作を、イベントのstopPropagationで無効化するPreventLexicalTripleClickExtensionを追加しました。これにより、ソフト改行を含む段落でのトリプルクリックや、トリプルクリックドラッグによる複数行選択が正常に動作するようになります。
背景
Lexicalはfacebook/lexical#4512にてトリプルクリック時の選択範囲を調整するハンドラを導入しました。このハンドラの目的は、ブラウザの「過剰選択(overselection)」動作への対処です。トリプルクリックによる選択範囲が次のブロックの先頭(offset 0)まで広がると、選択の変換処理で問題が起きる場合があり、その具体例として見出しツールが次の行も見出しに変換してしまう現象や、テーブルセルの削除が隣接セルに波及する現象が挙げられていました。
しかし、この対処は実際のユースケースを損なうものでした。2025年6月にはLexical本体にバグレポートfacebook/lexical#7592が提出されており、<br>タグによるソフト改行を含む段落でのトリプルクリック、およびトリプルクリックドラッグによる複数行選択が正常に動作しないことが報告されています。Lexxyは独自の見出しツールを持ち、Lexicalのハンドラでカバーしようとしていたテーブルセル削除の問題も既に解消されているため、このハンドラはLexxy上では害のみをもたらす状態でした。
技術的な変更
新たに追加されたPreventLexicalTripleClickExtensionが、キャプチャフェーズでclickイベントを捕捉し、Lexicalのバブリングフェーズのハンドラより先に実行されることで問題を解決します。
src/extensions/prevent_lexical_triple_click_extension.jsとして追加されたクラスは、LexxyExtensionを継承し、defineExtensionを介してLexicalの拡張機構に登録されます。editor.registerRootListenerでルートDOM要素にclickリスナーを設定し、{ capture: true }オプションによりキャプチャフェーズでの受信を宣言します。
export class PreventLexicalTripleClickExtension extends LexxyExtension {
get lexicalExtension() {
return defineExtension({
name: "lexxy/prevent-lexical-triple-click",
register: (editor) => editor.registerRootListener((rootElement) => {
if (rootElement) {
return registerEventListener(
rootElement,
"click",
this.#handleTripleClick.bind(this),
{ capture: true }
)
}
})
})
}
#handleTripleClick(event) {
if (event.detail === 3) {
event.stopPropagation()
}
}
}
ハンドラはevent.detail === 3、すなわちトリプルクリックのみを対象にstopPropagation()を呼び出します。これによりイベントはキャプチャフェーズで伝播を止め、LexicalのバブリングフェーズのonClickハンドラには届きません。ブラウザ本来のトリプルクリック選択動作はそのまま維持されます。
src/elements/editor.jsでは、既存の拡張機能リストにPreventLexicalTripleClickExtensionを追記する1行の変更のみで統合されています。
// 変更後
import { PreventLexicalTripleClickExtension } from "../extensions/prevent_lexical_triple_click_extension.js"
// ...
[
RewritableHistoryExtension,
AttachmentsExtension,
FormatEscapeExtension,
LinkOpenerExtension,
PreventLexicalTripleClickExtension // 追加
]
ブラウザテストtest/browser/tests/editor/prevent_lexical_triple_click.test.jsも合わせて追加されており、2つのシナリオを検証します。
- ソフト改行(
<br>)を含む段落内の1行をトリプルクリックしたとき、その視覚的な1行のみが選択されること - トリプルクリックドラッグで複数の行にまたがる選択が正常に行えること
テストコード内のtripleClickDrag関数は、ブラウザのクリックカウントを1→2→3とマウスイベントで段階的に送信し、ドラッグ操作を再現しています。
設計判断
イベント伝播をキャプチャフェーズで止める方式が採用されました。Lexicalのハンドラはバブリングフェーズに登録されているため、キャプチャフェーズでstopPropagation()を呼ぶことで、ブラウザのデフォルト選択動作を妨げることなくLexical固有のロジックのみを無効化できます。
PR本文では、問題の根本的な対処としてLexical本体への改善(選択処理ロジックをアプリケーションが上書きできるハンドラへ移動する、またはトリプルクリックハンドラを完全に削除するなど)を期待する姿勢が示されています。Lexxy側での対処はあくまで上流が修正されるまでの暫定措置として位置付けられており、Lexxyの見出しツールやテーブルが該当の問題を示さないという事実を根拠に、Lexicalのハンドラが提供していた保護を不要と判断しています。
まとめ
本PRは、Lexicalの上流実装の副作用をキャプチャフェーズのイベント遮断という最小限の手術で封じ込める変更です。Lexxy固有のツールが問題の原因となる動作を再現しないことを確認したうえで、ブラウザ本来の選択動作を優先する判断が下されており、上流での修正後に容易に差し戻せるよう拡張として独立したモジュールに切り出された点も注目されます。