矢印キーナビゲーションがカーソル位置によらず画像を正しく選択するよう修正
Up/Downキーで段落から画像(デコレーターノード)へ移動する際、カーソルがテキストの途中にある場合でも画像が確実に選択されるよう、DOM ジオメトリを用いたビジュアル行検出が追加されました。
背景
矢印キーによる画像選択が不安定という問題が報告されていました。カーソルが段落の先頭(offset 0)または末尾にある場合は画像が選択されるものの、単語の途中など中間オフセットにある場合はブラウザのネイティブナビゲーションが非編集可能な デコレーターノード をスキップしてしまうという挙動です。
#871 および #708 にて詳細が報告されており、@lylo によって「単語先頭では画像が選択されるが、単語途中からUpキーを押すと画像が選択されない」という動画付きの再現レポートが提供されています。ネイティブナビゲーションに委ねるのではなく、Lexxy側でビジュアル行の位置を判定して介入する方針が採られました。
技術的な変更
src/editor/selection.js に、カーソルがブロックの先頭または末尾のビジュアル行に位置しているかを DOM ジオメトリで判定するメソッド群が追加されました。
修正前のコードは、テキストノード内のオフセットのみで判断していました。
変更前:
// 下方向
if (offset < anchorNode.getTextContentSize()) return null
return this.#getNextNodeFromTextEnd(anchorNode)
// 上方向
if (offset > 0) return null
return this.#getPreviousNodeFromTextStart(anchorNode)
変更後:
// 下方向
if (offset === anchorNode.getTextContentSize()) return this.#getNextNodeFromTextEnd(anchorNode)
if (this.#isCursorOnLastVisualLineOfBlock(anchorNode)) {
const topLevelElement = anchorNode.getTopLevelElement()
return topLevelElement ? topLevelElement.getNextSibling() : null
}
return null
// 上方向
if (offset === 0) return this.#getPreviousNodeFromTextStart(anchorNode)
if (this.#isCursorOnFirstVisualLineOfBlock(anchorNode)) {
const topLevelElement = anchorNode.getTopLevelElement()
return topLevelElement ? topLevelElement.getPreviousSibling() : null
}
return null
オフセットが末尾(または先頭)と一致する場合は従来のパスを通り、一致しない場合に #isCursorOnLastVisualLineOfBlock / #isCursorOnFirstVisualLineOfBlock でビジュアル行の端かどうかを判定します。ビジュアル行の端であれば、トップレベル要素の次(または前)の兄弟ノードを返してデコレーターを選択します。ビジュアル行の端でなければ null を返し、ブラウザのネイティブナビゲーションに委ねます。
新たに追加された #isCursorOnEdgeLineOfBlock メソッドは、カーソルの Y 座標とブロック先頭・末尾の Y 座標を DOM ジオメトリで比較することで、カーソルが先頭または末尾のビジュアル行に位置しているかを判定します。コメントにも「Y 座標でブロックの edge line を比較する」と明記されており、テキストオフセットではなく視覚的な行位置を根拠にするアプローチです。
テスト面では test/browser/tests/attachments/arrow_navigation_attachment.test.js に 2 つのケースが追加されました。「Bottom paragraph」の 6 文字目(単語途中)にカーソルを置いてUpキーを押すと figure.attachment.node--selected が 1 件になることを確認するケースと、その対称となるDownキーのケースです。TreeWalker でテキストノードを直接操作して任意オフセットにカーソルを設定しており、回帰テストとして機能します。
設計判断
テキストオフセットによる判定からビジュアル行検出への移行 が本修正の核心です。
変更前の条件式(offset < size や offset > 0)は、「オフセットが末尾・先頭でなければブロックの端ではない」という仮定に基づいていました。しかし複数行段落ではこの仮定が崩れ、また単一行でもカーソルが視覚的に行の端にいる場合(単語途中であっても段落が 1 行しかない場合など)はデコレーターへ移動すべきです。DOM ジオメトリ(Y 座標比較)を使うことで、折り返しを含む任意の段落に対して「カーソルが視覚的な端の行にいるか」を正確に判定できます。
既存の末尾・先頭オフセットのパスは削除せず、完全一致の場合の早期リターンとして残しています。これにより、オフセットが確定的な場合はジオメトリ計算を省略できるため、不要なDOM操作を抑えつつ互換性を保つ設計になっています。
まとめ
テキストオフセットという構造上の情報だけでは「カーソルが視覚的な行の端にいるか」を正確に判定できないという根本的な限界に対処した修正です。DOM ジオメトリによるビジュアル行検出を組み合わせることで、カーソル位置によらずデコレーターノードへの矢印キーナビゲーションが安定して動作するようになります。