Paste 時にノード挿入コマンドをディスパッチしてフックできるようにした

basecamp/lexxy

Lexxy の貼り付け処理にフックポイントが追加され、insertHTML/insertDOM が実行される際に SELECTION_INSERT_CLIPBOARD_NODES_COMMAND がディスパッチされます。これにより、プラグインやカスタムロジックが生成されたノードや選択状態を介入・変更できるようになりました。

背景

貼り付けロジックが Contents に集中し始め、将来的なリファクタリングの足掛かりとなっていた点が背景です。従来はクリップボードから生成されたノードが直接エディタに挿入され、外部からの介入手段がありませんでした。この制約が拡張性の低下として指摘されていました。

この課題を解消するため、ノード生成後にフックを呼び出す機構が求められました。コマンドベースのディスパッチにすれば、既存の更新フローを壊さずにフックポイントを提供できます。結果として、Clipboard のリファクタリング準備が整いました。

まとめとして、貼り付け時にフックを差し込む設計は拡張性向上とコード分離の狙いを満たす選択肢でした。

技術的な変更

src/editor/clipboard.js では lexical から $onUpdateSELECTION_INSERT_CLIPBOARD_NODES_COMMAND を新たにインポートし、リンク挿入後のイベントディスパッチを $onUpdate 内で実行するよう変更しました。

-import { $createTextNode, $getSelection, $isParagraphNode, $isRangeSelection, COMMAND_PRIORITY_NORMAL, PASTE_COMMAND, PASTE_TAG, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND } from "lexical"
+import { $createTextNode, $getSelection, $isParagraphNode, $isRangeSelection, $onUpdate, COMMAND_PRIORITY_NORMAL, PASTE_COMMAND, PASTE_TAG, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND } from "lexical"

#handleParsedClipboardNodes のロジックが拡張され、URL が検出された場合に $isRangeSelection が true であることを確認してからリンクを挿入し、戻り値を true にします。これにより、範囲選択が存在しないケースでの不必要な処理が防がれます。

@@
-    if (!url) return false
-
-    this.#insertSingleLinkAt(selection, url)
-    return true
+    if (url && $isRangeSelection(selection)) {
+      this.#insertSingleLinkAt(selection, url)
+      return true
+    }

リンク挿入後のディスパッチは従来の Promise.resolve().then から $onUpdate に置き換えられ、エディタの更新サイクルと整合性が取れました。

-    Promise.resolve().then(() => this.#dispatchLinkInsertEvent(nodeKey, { url }))
+    $onUpdate(() => this.#dispatchLinkInsertEvent(linkNode.getKey(), { url }))

src/editor/contents.js では SELECTION_INSERT_CLIPBOARD_NODES_COMMAND をインポートし、貼り付け時の DOM 整形とコマンドディスパッチのロジックを新たに追加しました。insertDOM のフローが整理され、#formatPastedDOM#dispatchPastedNodesCommand がそれぞれ責務を担います。

@@
-  HISTORY_MERGE_TAG, PASTE_TAG
+  HISTORY_MERGE_TAG, PASTE_TAG,
+  SELECTION_INSERT_CLIPBOARD_NODES_COMMAND
@@
-      if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc)
+      if ($hasUpdateTag(PASTE_TAG)) this.#formatPastedDOM(doc)
@@
-      if (!this.#insertUploadNodes(nodes)) {
-        this.insertAtCursor(...nodes)
+      if (!$hasUpdateTag(PASTE_TAG) || !this.#dispatchPastedNodesCommand(nodes)) {
+        this.#insertUploadNodes(nodes) || this.insertAtCursor(...nodes)
+  #formatPastedDOM(doc) {
+    this.#unwrapPlaceholderAnchors(doc)
+    this.#stripTableCellColorStyles(doc)
+  }
+
+  #dispatchPastedNodesCommand(nodes) {
+    return this.editor.dispatchCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, {
+      nodes, selection: $getSelection()
+    })
+  }

テスト用のフィクスチャとテストケースも追加され、SELECTION_INSERT_CLIPBOARD_NODES_COMMAND が貼り付け時に正しくディスパッチされ、ハンドラの戻り値がデフォルト挿入の有無を制御できることが検証されています。

設計判断

コマンドディスパッチ を導入した最大の設計意図は、貼り付け処理をプラグインフレンドリーにすることです。ノード生成後に即座に実行されるコマンドは、外部が介入できる唯一の入口となり、既存ロジックを変更せずに機能拡張が可能になります。

$onUpdate の採用は、エディタの更新サイクルと同期させてフックを実行するという安全策です。これにより、貼り付けノードが DOM に反映される前にフックが走り、状態不整合のリスクが低減します。

コマンドハンドラは boolean を返すことでデフォルトの貼り付け動作を制御でき、後方互換性 を保ちつつ新たな制御フローを提供します。既存の貼り付けロジックはハンドラが false を返す限りそのまま動作し、true を返すと挿入が抑制されます。

まとめ

今回の PR で 貼り付け時に SELECTION_INSERT_CLIPBOARD_NODES_COMMAND がディスパッチ されるようになり、ノード生成後のカスタマイズポイントが公式に提供されました。コマンドベースの拡張は既存コードへの侵入を最小限に抑え、将来的な Clipboard のリファクタリングを円滑に進める土台となります。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
0805baae

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、まとめの各要素が揃っており、総論→各論→結論の流れが明確です。設計判断は任意要素として適切に配置されています。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは正しい ```:言語:ファイルパス``` 形式で記述され、GitHubリンクも PR 番号が正しくリンク化されています。

対象読者への適合性 ✓ PASS

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

エンジニア向けの技術的記述に留まり、初心者向けの過度な説明はありません。

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

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

各セクションは導入パラグラフ、詳細パラグラフ、まとめパラグラフで構成され、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供された Diff と完全に一致しており、ファイル名・変更箇所・追加コードが正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語は PR 内容と合致しており、誤用や不適切な用語は見受けられません。

説明の技術的正確性 ✓ PASS

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

技術的主張は Diff と PR の記述に基づいており、因果関係や効果の説明に矛盾はありません。

事実の突合 ✓ PASS

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

背景や変更の意図・効果は PR の説明と合致し、PR に記載されていない推測や外部知識は含まれていません。

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

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

PR 番号 #1093 など数値・固有名詞は正確に記載されています。

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

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

記事タイトルは PR タイトルの意味を日本語で適切に置き換えており、一致しています。

外部知識の正確性 ✓ PASS

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

バージョンやリリース情報等、PR に無い外部知識は一切記載されていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と整合しています。