Lexicalのカーソル制御を改善するProvisionalParagraphの導入

basecamp/lexxy

Lexical(Lexxy)でDecoratorNodeとエディタ境界の間のカーソル選択を制御する ProvisionalParagraphNode が導入されました。これは出力なしにカーソルのターゲットとなる暫定的な段落ノードで、画像ギャラリーなどのDecoratorNode周辺での選択体験を向上させます。

背景

Lexicalは以前、DecoratorNodeとエディタ境界の間での選択を処理する際に偽のカーソルを表示していました。この動作は見た目が不自然で、特に画像ギャラリー(#716で導入予定)で最適とは言えない振る舞いをしていました。

DecoratorNodeや非テキストのElementNodeは通常のテキスト選択をサポートしないため、これらのノードがエディタの端やノード同士の隣接部に配置された場合、カーソルの配置場所が曖昧になる問題がありました。この問題は空のエディタや、ノード間に<p>タグを挿入して流動的な選択を確保するといった複数のエッジケースを引き起こしていました。

技術的な変更

ProvisionalParagraphNodeParagraphNode を拡張したノードで、以下の特徴を持ちます:

export class ProvisionalParagraphNode extends ParagraphNode {
  static neededBetween(nodeBefore, nodeAfter) {
    return !$isSelectableElement(nodeBefore, "next")
      && !$isSelectableElement(nodeAfter, "previous")
  }

  getTextContent() {
    return ""
  }

  exportDOM() {
    return {
      element: null
    }
  }
}

このノードは getTextContent() が空文字を返し、exportDOM()null を返すことで、HTML出力に影響を与えません。選択状態に応じてCSSクラス hidden が切り替わり、選択されていない時は最小限の高さ(0.5ch)で表示されます:

p.provisional-paragraph {
  display: block;

  &.hidden {
    block-size: 0.5ch;
    margin: 0;
  }
}

ProvisionalParagraphは以下のタイミングで自動的に挿入されます:

  • DecoratorNodeまたは非テキストElementNodeがエディタの上下端または互いに隣接している場合
  • エディタが空の場合

ProvisionalParagraphExtensionRootNode の変更を監視し、$insertRequiredProvisionalParagraphs で必要な箇所にノードを挿入します:

function $insertRequiredProvisionalParagraphs(rootNode) {
  const firstNode = rootNode.getFirstChild()
  if (ProvisionalParagraphNode.neededBetween(null, firstNode)) {
    $insertFirst(rootNode, new ProvisionalParagraphNode)
  }

  for (const node of $firstToLastIterator(rootNode)) {
    const nextNode = node.getNextSibling()
    if (ProvisionalParagraphNode.neededBetween(node, nextNode)) {
      node.insertAfter(new ProvisionalParagraphNode)
    }
  }
}

ユーザーがProvisionalParagraphで編集を開始すると、通常の ParagraphNode に変換されます。また、不要になったProvisionalParagraphは自動的に削除されます。

設計判断

Lexicalの新しい static transform API を活用してトランスフォーム処理をノード自身に定義する方式が採用されました。

従来は外部でノードの変換処理を管理していましたが、$config() メソッド内で $transform を定義することで、ノードのロジックを一箇所に集約できます:

$config() {
  return this.config("provisonal_paragraph", {
    extends: ParagraphNode,
    importDOM: () => null,
    $transform: (node) => {
      node.concretizeIfEdited(node)
      node.removeUnlessRequired(node)
    }
  })
}

この設計により、編集時の具体化(concretizeIfEdited)と不要時の削除(removeUnlessRequired)をノードクラス内で完結させています。コードの可読性と保守性が向上する一方、ノードの責任範囲が広がるトレードオフがあります。

また、deleteSelectedNodes() メソッドが src/editor/contents.js から削除され、選択されたノードの削除処理が簡素化されました。ProvisionalParagraphの自動管理により、従来のように明示的にノードを削除して選択を調整する必要がなくなっています。

まとめ

本PRは、Lexicalのカーソル制御における長年の課題を解決する基盤を提供します。ProvisionalParagraphNodeの導入により、DecoratorNode周辺の選択が自然になり、偽のカーソルを表示する回避策が不要になりました。エディタの空状態処理やノード間への<p>挿入といったエッジケースも整理され、画像ギャラリーなどの複雑なコンポーネントの実装が容易になります。

記事メタデータ

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:path/to/file.js```)およびGitHubのPR番号リンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Lexicalの内部実装に関する詳細な解説であり、専門知識を持つエンジニアという対象読者に完全に適合しています。初心者向けの過度な説明はありません。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiffの内容と正確に一致しています。省略や改変も説明を補助する適切な範囲です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「DecoratorNode」「ProvisionalParagraphNode」「static transform API」など、Lexicalに関連する技術用語が正確かつ文脈に即して使用されています。

説明の技術的正確性 ✓ PASS

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

ProvisionalParagraphNodeの役割、DOMへの非出力、CSSによる表示制御、不要なメソッドの削除など、技術的な説明はすべてDiffとPR情報によって裏付けられており、正確です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのタイトル、Description、Diffの内容に基づいており、根拠のない推測や創作(ハルシネーション)は見られません。

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

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

PR番号(#726)、関連PR番号(#716)、ファイルパスなどの数値や固有名詞はすべて正確に記載されています。

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

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

記事のタイトル「Lexicalのカーソル制御を改善するProvisionalParagraphの導入」は、PRの主題を的確かつ分かりやすく要約しています。

外部知識の正確性 ✓ PASS

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

記事には、PR情報に記載のないバージョンサポート状況やリリース日程などの外部知識は含まれていません。すべての情報が提供された資料に基づいています。

時間表現の正確性 ✓ PASS

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

「[#716]で導入予定」という時間表現は、PR Descriptionの「(to be introduced in #716)」と正確に一致しており、時間表現の歪曲はありません。