DecoratorNodeの選択処理をCLICK_COMMANDベースに移行
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() })
}
ActionTextAttachmentNode、CustomActionTextAttachmentNode、HorizontalDividerNode の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_COMMAND は KEY_DELETE_COMMAND と KEY_BACKSPACE_COMMAND の両方を抽象化したLexical標準のコマンドです。
これらの変更により、dispatchCustomEvent 関数と、システムテストヘルパーの wait_for_node_selection 関数が完全に削除されました。コード削減量は60行の削除に対して24行の追加となり、差し引き36行の削減です。
設計判断
Lexicalの標準コマンドシステムへの統一 が優先されました。カスタムイベントシステムは柔軟性がある一方で、フレームワーク外の独自実装として保守コストを増加させます。#727 では、CLICK_COMMAND と DELETE_CHARACTER_COMMAND というLexicalの標準コマンドを活用することで、フレームワークの更新追従性を高めています。
DOM要素からノードへの逆引きは $getNearestNodeFromDOMNode に委ねる方針 です。変更後のコードでは、クリックされたDOM要素からDecoratorNodeを特定する処理をLexical APIに任せており、ノード側でキー情報を保持する必要がなくなりました。この判断により、各DecoratorNodeの実装が単純化され、DOMとノードの対応関係の管理がフレームワーク側に一元化されています。
テストコードからの非同期待機処理の削除 も注目すべき点です。test/system/attachments_test.rb と test/system/horizontal_divider_test.rb から wait_for_node_selection の呼び出しが削除されました。カスタムイベントベースでは nextFrame() による非同期待機が必要でしたが、コマンドハンドラベースではLexicalの更新サイクル内で同期的に処理が完結するため、テストコードがシンプルになっています。
まとめ
本PRは、内部イベントシステムからLexical標準コマンドへの移行により、コードの複雑性を削減しフレームワーク依存度を高めた変更です。カスタムイベントの完全削除と削除コマンドの統一により、DecoratorNodeの実装がより宣言的になり、Lexicalのアップデートへの追従も容易になっています。