Lexicalエディタの選択範囲APIに書式判定メソッドとノード検索ヘルパーを追加
本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.js に getFormat() メソッドを実装しました。このメソッドは現在の選択範囲から書式情報を抽出し、構造化されたオブジェクトとして返します。
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.js に nearestNodeOfType(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の標準ユーティリティへの統一により、メンテナンス性も向上しています。