クリップボード経由でリンクをペーストするとURLが消える問題を修正
Lexxy内でコピーしたリンクを貼り付けるとhrefが失われプレーンテキストになるバグを修正しました。Clipboardクラスがペースト処理を直接ハンドリングする構造に変更され、「シングルリンク」ペイロードを検知して適切にリンクを復元します。
背景
Lexicalエディタのクリップボードペイロードがシングルリンクとして構成されたとき、Lexicalのデフォルトリッチテキストペースト処理がリンクを失う問題がありました。application/x-lexical-editor形式でコピーされたリンクは、単一の LinkNode(または段落でラップされたLinkNode)としてシリアライズされます。しかし、LexicalのデフォルトのSELECTION_INSERT_CLIPBOARD_NODES_COMMANDハンドラはこの構造をフラット化し、hrefを保持せずリンクのテキストコンテンツのみを挿入していました。
既存の実装では、ペースト処理を CommandDispatcher が PASTE_COMMAND に COMMAND_PRIORITY_LOW で登録し、clipboard.paste() に委譲する構造でした。このアーキテクチャでは、Lexicalが内部的に発火させる SELECTION_INSERT_CLIPBOARD_NODES_COMMAND を横断してリンクの特殊ケースを処理する経路がありませんでした。
この問題は、生のURLをペーストする既存のパスが正しく動作していたことと対照的です。URLペースト処理ではテキストを選択していればリンクでラップし、選択がなければカーソル位置にリンクを挿入する #insertSingleLinkAt が実装済みでした。今回の修正はその既存ロジックを再利用する形で解決を図っています。
技術的な変更
Clipboardクラス自身が PASTE_COMMAND と SELECTION_INSERT_CLIPBOARD_NODES_COMMAND の両コマンドを登録する責務を持つように変更され、CommandDispatcher からペースト関連のコード一切が除去されました。
clipboard.js では #registerPasteCommands メソッドが追加され、コンストラクタから呼び出されます。また、リソース管理のために ListenerBin を使ったクリーンアップ機構と dispose() メソッドが追加されています。
#registerPasteCommands() {
this.#listeners.track(
this.editor.registerCommand(PASTE_COMMAND, this.paste.bind(this), COMMAND_PRIORITY_NORMAL),
this.editor.registerCommand(
SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
(payload) => this.#handleParsedClipboardNodes(payload),
COMMAND_PRIORITY_NORMAL
)
)
}
#handleParsedClipboardNodes({ nodes, selection }) {
const url = $bareUrlFromSingleLink(nodes)
if (!url) return false
this.#insertSingleLinkAt(selection, url)
$bareUrlFromSingleLink ヘルパーが「シングルリンク」ペイロードを判定します。nodes配列がシングルリンクノードである場合、または単一の段落ノードにラップされた単一リンクノードである場合にURLを返し、それ以外は false を返してデフォルトのペースト処理に委ねます。$isLinkNode・$isParagraphNode を使った判定により、段落でラップされたリンクにも対応しています。
command_dispatcher.js では PASTE_COMMAND の登録、dispatchPaste メソッド、this.clipboard への参照がすべて削除されました。editor.js では Clipboard インスタンスが this.#disposables に追加され、dispose() が確実に呼ばれるようになっています。
this.clipboard = new Clipboard(this)
+this.#disposables.push(this.clipboard)
変更の要点を整理すると次のとおりです:
-
PASTE_COMMANDの優先度がCOMMAND_PRIORITY_LOWからCOMMAND_PRIORITY_NORMALに昇格 -
SELECTION_INSERT_CLIPBOARD_NODES_COMMANDをCOMMAND_PRIORITY_NORMALでインターセプト -
Clipboardがdispose()を実装し#disposablesに登録されるようになった - テストファイル
test/browser/tests/paste/copy_link.test.jsが追加(177行)
設計判断
ペースト処理の責務を CommandDispatcher から Clipboard クラスに移した設計が採用されました。
変更前の構造では CommandDispatcher がペーストのエントリポイントを持ち、Clipboard はそこから呼ばれるだけでした。今回の変更では Clipboard 自身がLexicalのコマンドバスに接続し、PASTE_COMMAND と SELECTION_INSERT_CLIPBOARD_NODES_COMMAND の両方を直接ハンドルします。これにより、Lexicalがクリップボードノードを解析した後に発火する SELECTION_INSERT_CLIPBOARD_NODES_COMMAND をインターセプトし、ノード構造を検査してシングルリンクを検出するという処理が自然に書けるようになります。
シングルリンク判定が false を返した場合はハンドラが false を返すことで、Lexicalのデフォルト処理にフォールスルーします。これにより既存のペースト動作を壊さずに特定ケースのみを上書きできます。ListenerBin による登録解除の管理もこの変更で整備され、ライフサイクル管理の一貫性が向上しています。
まとめ
ペースト処理の責務を Clipboard クラスに集約し、Lexicalのコマンドパイプラインを適切なタイミングでインターセプトすることで、シングルリンクのペイロードが持つ構造情報を保持できるようになりました。既存のURLペーストロジックを再利用する設計は変更範囲を最小化しつつ、リンクの段落ラップなど複数の形式に対応する堅牢な解決策を実現しています。