Lexicalエディタの選択範囲APIに書式判定メソッドとノード検索ヘルパーを追加

basecamp/lexxy

本PRは、選択範囲の書式情報を取得する selection.getFormat() メソッドと、特定のノード型を検索する selection.nearestNodeOfType() ヘルパーを導入しました。これにより、Webツールバーとネイティブツールバーの両方が統一された選択範囲APIを利用できるようになります。

背景

既存コードでは、ツールバー要素が選択範囲の書式状態(太字、イタリック、リンク内かどうかなど)を個別に判定していました。src/elements/toolbar.js#update() メソッド内で、選択範囲のノードから書式情報を直接抽出する処理が重複して実装されていたため、Webツールバーとネイティブツールバーで同じロジックを共有できない状態でした。

また、特定の型のノードを検索するパターンも繰り返し出現していました。例えば、リスト項目ノードを取得する getNearestListItemNode() のような専用ヘルパー関数が src/helpers/lexical_helper.js に定義されていましたが、これはLexicalの標準ユーティリティ $getNearestNodeOfType() で代替可能な処理でした。

技術的な変更

selection.getFormat() メソッドの追加

src/editor/selection.jsgetFormat() メソッドを実装しました。このメソッドは現在の選択範囲から書式情報を抽出し、構造化されたオブジェクトとして返します。

getFormat() {
  const selection = $getSelection()
  if (!$isRangeSelection(selection)) return {}

  const anchorNode = selection.anchor.getNode()
  if (!anchorNode.getParent()) return {}

  const topLevelElement = anchorNode.getTopLevelElementOrThrow()
  const listType = getListType(anchorNode)

  return {
    isBold: selection.hasFormat("bold"),
    isItalic: selection.hasFormat("italic"),
    isStrikethrough: selection.hasFormat("strikethrough"),
    isHighlight: isSelectionHighlighted(selection),
    isInLink: $getNearestNodeOfType(anchorNode, LinkNode) !== null,
    isInQuote: $isQuoteNode(topLevelElement),
    isInHeading: $isHeadingNode(topLevelElement),
    isInCode: selection.hasFormat("code") || $getNearestNodeOfType(anchorNode, CodeNode) !== null,
    isInList: listType !== null,
    listType,
    isInTable: $getTableCellNodeFromLexicalNode(anchorNode) !== null
  }
}

返却されるオブジェクトには、太字・イタリック・取り消し線といった基本的なテキスト書式に加え、リンク・引用・見出し・コード・リスト・テーブル内にいるかどうかの構造情報が含まれます。listType プロパティは、リスト内にいる場合にそのリストの種類(bullet または number)を提供します。

selection.nearestNodeOfType() ヘルパーの追加

同じく src/editor/selection.jsnearestNodeOfType(nodeType) メソッドを実装しました。このメソッドは、現在の選択範囲から指定された型の最も近い親ノードを検索します。

nearestNodeOfType(nodeType) {
  const anchorNode = $getSelection()?.anchor?.getNode()
  return anchorNode ? $getNearestNodeOfType(anchorNode, nodeType) : null
}

内部ではLexicalの $getNearestNodeOfType() ユーティリティを呼び出しますが、選択範囲の取得とnullチェックを内包することで、呼び出し側のコードを簡潔にします。

既存コードのリファクタリング

src/elements/toolbar.js では、#update() メソッド内で行っていた書式判定処理を selection.getFormat() の呼び出しに置き換えました。

変更前:

const isBold = selection.hasFormat("bold")
const isItalic = selection.hasFormat("italic")
const isStrikethrough = selection.hasFormat("strikethrough")
const isHighlight = isSelectionHighlighted(selection)
const isInLink = this.#isInLink(anchorNode)
const isInQuote = $isQuoteNode(topLevelElement)
const isInHeading = $isHeadingNode(topLevelElement)
const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code")
const isInList = this.#isInList(anchorNode)
const listType = getListType(anchorNode)
const isInTable = $getTableCellNodeFromLexicalNode(anchorNode) !== null

変更後:

const { isBold, isItalic, isStrikethrough, isHighlight, isInLink, isInQuote, isInHeading,
  isInCode, isInList, listType, isInTable } = this.selection.getFormat()

このリファクタリングにより、ツールバーの実装は選択範囲の内部状態を直接操作せず、抽象化されたAPIを通じて書式情報を取得するようになりました。

src/helpers/lexical_helper.js では、カスタムヘルパー関数 getNearestListItemNode() を削除し、呼び出し側を標準ユーティリティに変更しました。

変更前:

const listItem = getNearestListItemNode(node)

変更後:

const listItem = $getNearestNodeOfType(node, ListItemNode)

同様に、getListType() の実装も簡略化され、$getNearestNodeOfType() を活用するようになりました。

設計判断

本PRは、選択範囲の状態判定ロジックをSelectionクラスに集約する方向性を示しています。

PR #743 では nearestNodeOfType() のみを導入する案が提示されていましたが、本PR #744 ではそれに加えて getFormat() も同時に実装されました。これは、ネイティブツールバーの実装においても同じ書式判定ロジックが必要になることが予見されていたためと考えられます。

カスタムヘルパー関数を削除し、Lexicalの標準ユーティリティを優先する判断も行われています。getNearestListItemNode() のような特定のノード型に特化したヘルパーを廃止することで、コードベースの一貫性が向上し、Lexicalのアップデートによる恩恵を受けやすくなります。

まとめ

本PRは、選択範囲の書式情報取得とノード検索のAPIを統一し、ツールバー実装間でのコード共有を可能にしました。selection.getFormat() による書式情報の構造化と selection.nearestNodeOfType() による検索APIの簡略化により、今後追加されるツールバー(ネイティブツールバーなど)も同じインターフェースを利用できます。また、Lexicalの標準ユーティリティへの統一により、メンテナンス性も向上しています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という3部構成が明確に適用されており、すべての必須要素が含まれています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:filepath)およびGitHubのPRリンク記法([#744](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Lexicalエディタの内部APIに関する専門的な内容であり、対象読者であるエンジニアに適した技術レベルと表現で書かれています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション内が総論→各論で構成され、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事で引用されているコードブロック(getFormat, nearestNodeOfType, toolbar.jsの変更前後など)は、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

LexicalのAPI名($getNearestNodeOfType, $isQuoteNodeなど)や関連用語が正確に使用されており、PR情報とも整合性が取れています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

コード変更の目的(ツールバー間のロジック共有)や実装内容の説明が技術的に正確で、Diffの内容と論理的に一致しています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescription(例: webとnativeツールバーでの利用)やDiffの内容に基づいており、根拠のない創作や推測は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#744, #743)が正確に記載され、正しくリンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「Lexicalエディタの選択範囲APIに書式判定メソッドとノード検索ヘルパーを追加」は、PRの主題「Selection helpers」を具体的に反映しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれない外部知識(バージョンサポート状況、リリース日程など)の追記はなく、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現は過去の変更内容を記述しており、PRの文脈と一致しています。時間的な歪曲はありません。