選択範囲端点がブロック境界にあるときのコードブブロック・引用ラップの誤動作を修正
この PR は、選択範囲のアンカーが直前の段落の末尾に止まっているケースで、コードブロックや引用ボタンを実行すると意図せず上位段落がラップされてしまう問題を解消します。選択範囲の端点を実際に内容があるブロックへシフトさせる新しいヘルパー関数を導入し、テストで振る舞いを検証しています。
背景
ブラウザがテキスト選択を生成する際、選択開始点(anchor) が前のブロックの終端に固定されることがあります。そのまま $expandSelectionToLineBreaksAndSplitAtEdges が呼び出されると、空のブロックが選択範囲に含まれ、コードブロックや引用のラップ対象に誤って取り込まれていました。特にコメント作成画面で、見出しやタイトル直下に貼り付けた段落を選択すると、タイトル行までがコードブロックに変換される不具合が報告されていました(Basecamp カード #10018915941)。この問題は選択範囲の境界処理がブロックエッジを正しく判定できていなかったことが根本原因です。
技術的な変更
$shrinkSelectionPastBlockEdges という新関数を src/helpers/lexical_helper.js に追加し、選択範囲の端点がブロックエッジに接している場合に隣接ブロックへ再定位させます。関数は以下のロジックで動作します。
function $shrinkSelectionPastBlockEdges(selection) {
if (selection.isCollapsed()) return
const anchorBlock = selection.anchor.getNode().getTopLevelElement()
const focusBlock = selection.focus.getNode().getTopLevelElement()
if (!anchorBlock || !focusBlock || anchorBlock.is(focusBlock)) return
if ($isAtBlockEnd(selection.anchor, anchorBlock)) {
const nextBlock = anchorBlock.getNextSibling()
if (nextBlock) selection.anchor.set(nextBlock.getKey(), 0, "element")
}
if ($isAtBlockStart(selection.focus, focusBlock)) {
const previousBlock = focusBlock.getPreviousSibling()
if (previousBlock) selection.focus.set(previousBlock.getKey(), previousBlock.getChildrenSize(), "element")
}
}
このヘルパーは $expandSelectionToLineBreaksAndSplitAtEdges の冒頭で呼び出され、選択範囲が拡張される前に端点を正しいブロックへ移動させます。エッジ判定は $isAtBlockEnd と $isAtBlockStart を介して実装され、テキストポイントが実際にブロック境界にあるかどうかを慎重に評価します。
テスト側では、段落上部がタイトル行である HTML を設定し、アンカーをタイトルの末尾、フォーカスを貼り付けた段落の末端に合わせた選択範囲を手動で構築しています。quote ボタンとコードブロック挿入ボタンの両方で、上位段落が誤ってラップされないことを assertEditorHtml で確認しています。
await clickToolbarButton(page, "insertCodeBlock")
await assertEditorHtml(
editor,
'<p>ABCD:</p><pre data-language="plain" data-highlight-language="plain">pasted one<br>pasted two</pre>'
)
同様に quote ボタンのテストでも期待通り <blockquote> が生成され、タイトル行はそのまま残ります。
設計判断
今回の修正は 「選択端点のエッジ処理をヘルパー関数に切り出す」 という設計方針をとっています。元の $expandSelectionToLineBreaksAndSplitAtEdges にロジックを直接埋め込むことも可能でしたが、エッジ判定は他の選択操作でも再利用できるため、独立した関数に分離することでコードの可読性と保守性を高めました。加えて、端点が同一ブロックにある場合は早期リターンする guard 条件を設け、不要な走査コストを回避しています。これにより、既存の選択拡張ロジックへの影響は最小限に抑えられました。
まとめ
選択範囲のアンカーがブロック末端にあるケースで、コードブロックや引用ラップが誤って前段落まで含んでしまうバグは、端点を実際のコンテンツがあるブロックへシフトさせる $shrinkSelectionPastBlockEdges の導入で解決しました。テストで両機能の正しい振る舞いが保証され、将来的に同様の境界処理が必要な機能でも再利用できる基盤ができました。