ギャラリーへのドロップ時に元のカーソル位置を保持する修正
OSからファイルをドラッグ&ドロップでイメージギャラリーに挿入する際、カーソルの右側へのドロップで画像が誤った位置に挿入されていたバグを修正しました。ドラッグ開始時の選択状態を保存・復元することで、ドロップ先に関わらず常に元のカーソル位置へ画像が挿入されるようになります。
背景
ドラッグ操作中に発生するブラウザの selectionchange イベントが、Lexicalの内部選択状態を意図せず移動させることが根本原因でした。ユーザーがカーソルをギャラリー内の特定位置(例:2枚の画像の間)に置いていても、OSからファイルをドラッグしてエディタ上に移動させると、ブラウザが生成するドラッグキャレットの動きに追従して selectionchange が発火し、Lexicalの選択状態が上書きされてしまいます。
その結果、drop イベントが発火した時点では選択状態がドラッグキャレットの最終位置を指しており、ファイルはユーザーが意図した位置ではなくドラッグキャレットが止まった位置に挿入されていました。この問題は特にカーソルの右側にドロップした場合に顕在化します。
技術的な変更
CommandDispatcher クラスに選択状態のスナップショット機構が追加されました。新たに導入されたプライベートフィールド #selectionBeforeDrag に、ドラッグ開始時点の選択状態を保持します。
src/editor/command_dispatcher.js への変更は以下の3点です:
-
#handleDragEnter:dragCounterが初めて1になったとき(エディタへの最初のドラッグ進入時)に#saveSelectionBeforeDrag()を呼び出す -
#handleDragLeave:dragCounterが0に戻ったとき(ドロップなしでエディタ外へ離脱したとき)に#selectionBeforeDragをnullにクリアする -
#handleDrop:uploadFiles()の呼び出し前に#restoreSelectionBeforeDrag()を呼び出して選択状態を復元する
#saveSelectionBeforeDrag() {
this.editor.getEditorState().read(() => {
this.#selectionBeforeDrag = $getSelection()?.clone()
})
}
#restoreSelectionBeforeDrag() {
if (!this.#selectionBeforeDrag) return
this.editor.update(() => {
$setSelection(this.#selectionBeforeDrag)
})
this.#selectionBeforeDrag = null
}
選択状態の保存には editor.getEditorState().read() を、復元には editor.update() を使用しています。これはLexicalの読み取りと書き込みのAPIを正しく使い分けたパターンで、$getSelection()?.clone() によってスナップショットが値コピーとして保持されます。また、$setSelection のインポートも #restoreSelectionBeforeDrag の実装に合わせて追加されています。
設計判断
ドラッグの「入口」でスナップショットを取り、「出口」で復元する対称的な設計 が採用されています。
既存の dragCounter はネストしたドラッグイベント(子要素への再進入で dragleave が誤発火する問題への対処)の管理に使われていましたが、今回の修正はこの仕組みに乗っています。dragCounter === 1 になった最初の dragenter 時点だけ選択状態を保存することで、子要素を横断するドラッグ中に何度も上書きされることを防いでいます。また、ドロップされずに離脱した場合(dragCounter === 0)は null をセットしてクリアするため、次回のドラッグ操作に前回の選択状態が持ち越されることもありません。
#restoreSelectionBeforeDrag 内で復元後に即座に null をセットしている点も、一度の drop でスナップショットが消費されることを保証しており、状態の漏洩を防いでいます。
まとめ
ドラッグ操作中のブラウザイベントによるLexical内部状態の汚染を、「進入時スナップショット→離脱時クリア→ドロップ時復元」というライフサイクル管理で解決した修正です。既存の dragCounter 管理ロジックと整合した設計により、最小限のコード追加でブラウザの副作用を透過的に吸収しています。