DecoratorNodeの選択処理をCLICK_COMMANDベースに移行

basecamp/lexxy

LexxyのDecoratorNode選択処理が、カスタムイベントベースからLexical標準の CLICK_COMMAND ハンドラベースに変更されました。この変更により、内部イベントシステムへの依存が解消され、コードの複雑性が大幅に削減されています。

背景

これまでDecoratorNode(添付ファイルや水平線などのノード)の選択は、各ノードのDOM要素に個別の click イベントリスナーを設定し、lexxy:internal:select-node というカスタムイベントをディスパッチする方式でした。このアプローチでは、ノード側でイベント発火処理を実装し、エディタ側でそのイベントを購読して選択状態を更新するという二段構えの実装が必要でした。カスタムイベントシステムの保守コストと、Lexicalの標準的なコマンドシステムとの二重管理が課題となっていました。

技術的な変更

選択処理の実装が CLICK_COMMAND ハンドラに統一されました。 src/editor/selection.js のイベントリスナー登録が以下のように変更されています:

変更前:

#listenForNodeSelections() {
  this.editor.getRootElement().addEventListener("lexxy:internal:select-node", async (event) => {
    await nextFrame()

    const { key } = event.detail
    this.editor.update(() => {
      const node = $getNodeByKey(key)
      if (node) {
        const selection = $createNodeSelection()
        selection.add(node.getKey())
        $setSelection(selection)
      }
      this.editor.focus()
    })
  })
}

変更後:

#listenForNodeSelections() {
  this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
    if (!isDOMNode(target)) return false

    // CLICK_COMMAND内で直接DecoratorNodeを判定・選択
  }, COMMAND_PRIORITY_LOW)
}

この変更により、カスタムイベント lexxy:internal:select-node の発火処理が不要になり、各DecoratorNodeから以下のコードが削除されました:

// 削除されたコード
figure.addEventListener("click", () => {
  this.#select(figure)
})

#select(figure) {
  dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() })
}

ActionTextAttachmentNodeCustomActionTextAttachmentNodeHorizontalDividerNode の3つのノードすべてから同様のイベントハンドラが削除されています。

ノード選択ヘルパー関数の追加により、選択状態の構築が簡潔になりました。 src/helpers/lexical_helper.js に以下の関数が追加されています:

export function $createNodeSelectionWith(...nodes) {
  const selection = $createNodeSelection()
  nodes.forEach(node => selection.add(node.getKey()))
  return selection
}

この関数により、複数ノードを含む選択状態の構築が一行で記述できるようになりました。

削除コマンドの処理が DELETE_CHARACTER_COMMAND に統一されました。 キー入力による削除処理の登録が以下のように変更されています:

// 変更前
this.editor.registerCommand(KEY_DELETE_COMMAND, this.#deleteSelectedOrNext.bind(this), COMMAND_PRIORITY_LOW)
this.editor.registerCommand(KEY_BACKSPACE_COMMAND, this.#deletePreviousOrNext.bind(this), COMMAND_PRIORITY_LOW)

// 変更後
this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW)

DELETE_CHARACTER_COMMANDKEY_DELETE_COMMANDKEY_BACKSPACE_COMMAND の両方を抽象化したLexical標準のコマンドです。

これらの変更により、dispatchCustomEvent 関数と、システムテストヘルパーの wait_for_node_selection 関数が完全に削除されました。コード削減量は60行の削除に対して24行の追加となり、差し引き36行の削減です。

設計判断

Lexicalの標準コマンドシステムへの統一 が優先されました。カスタムイベントシステムは柔軟性がある一方で、フレームワーク外の独自実装として保守コストを増加させます。#727 では、CLICK_COMMANDDELETE_CHARACTER_COMMAND というLexicalの標準コマンドを活用することで、フレームワークの更新追従性を高めています。

DOM要素からノードへの逆引きは $getNearestNodeFromDOMNode に委ねる方針 です。変更後のコードでは、クリックされたDOM要素からDecoratorNodeを特定する処理をLexical APIに任せており、ノード側でキー情報を保持する必要がなくなりました。この判断により、各DecoratorNodeの実装が単純化され、DOMとノードの対応関係の管理がフレームワーク側に一元化されています。

テストコードからの非同期待機処理の削除 も注目すべき点です。test/system/attachments_test.rbtest/system/horizontal_divider_test.rb から wait_for_node_selection の呼び出しが削除されました。カスタムイベントベースでは nextFrame() による非同期待機が必要でしたが、コマンドハンドラベースではLexicalの更新サイクル内で同期的に処理が完結するため、テストコードがシンプルになっています。

まとめ

本PRは、内部イベントシステムからLexical標準コマンドへの移行により、コードの複雑性を削減しフレームワーク依存度を高めた変更です。カスタムイベントの完全削除と削除コマンドの統一により、DecoratorNodeの実装がより宣言的になり、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の存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という理想的な「総論→各論→結論」の構成が明確に適用されています。各セクションの役割も明確で、非常に理解しやすいです。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```言語:ファイルパス)やPR番号のリンク記法([#727](URL))がガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

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

Lexicalフレームワークに関する専門知識を持つエンジニアを対象としており、専門用語の使い方も適切です。過度な説明がなく、簡潔に要点がまとめられています。

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

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

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。特に「技術的な変更」や「設計判断」セクションでは、太字を使ってトピックセンテンスが強調されており、視覚的にも理解しやすいです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiffの内容と正確に一致しています。コードの追加・削除箇所やファイル名も正確に反映されており、信頼性が高いです。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「DecoratorNode」「CLICK_COMMAND」「$getNearestNodeFromDOMNode」など、Lexicalに関連する技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

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

カスタムイベントからコマンドシステムへの移行という変更の核心部分が、Diffの内容に基づいて技術的に正確に説明されています。因果関係も論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのタイトル、Description、およびDiff内のコード変更によって裏付けられています。特に「設計判断」セクションは、コードから意図を正確に読み取っており、ハルシネーションは見られません。

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

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

PR番号(#727)やコードの増減行数(60行削除、24行追加)など、記事に含まれる数値や固有名詞はすべてPR情報と一致しており、正確です。

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

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

記事のタイトル「DecoratorNodeの選択処理をCLICK_COMMANDベースに移行」は、PRの主題を具体的かつ正確に要約しており、内容との一貫性も保たれています。

外部知識の正確性 ✓ PASS

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

記事は提供されたPR情報に忠実であり、バージョンのサポート状況やリリース日程といった、PRに記載のない外部知識を持ち込んでいません。

時間表現の正確性 ✓ PASS

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

記事内に時間表現の歪曲は見られず、PRの事実関係を正確に伝えています。