Paste 時にノード挿入コマンドをディスパッチしてフックできるようにした
Lexxy の貼り付け処理にフックポイントが追加され、insertHTML/insertDOM が実行される際に SELECTION_INSERT_CLIPBOARD_NODES_COMMAND がディスパッチされます。これにより、プラグインやカスタムロジックが生成されたノードや選択状態を介入・変更できるようになりました。
背景
貼り付けロジックが Contents に集中し始め、将来的なリファクタリングの足掛かりとなっていた点が背景です。従来はクリップボードから生成されたノードが直接エディタに挿入され、外部からの介入手段がありませんでした。この制約が拡張性の低下として指摘されていました。
この課題を解消するため、ノード生成後にフックを呼び出す機構が求められました。コマンドベースのディスパッチにすれば、既存の更新フローを壊さずにフックポイントを提供できます。結果として、Clipboard のリファクタリング準備が整いました。
まとめとして、貼り付け時にフックを差し込む設計は拡張性向上とコード分離の狙いを満たす選択肢でした。
技術的な変更
src/editor/clipboard.js では lexical から $onUpdate と SELECTION_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 のリファクタリングを円滑に進める土台となります。