`contenteditable=false` を廃止し、CLICK_COMMANDでリンク開封を実装
Ctrl/Cmd+クリックによるリンク開封の実装を、contenteditable=false のトグルから Lexical の CLICK_COMMAND ベースに置き換えました。これによりカーソル移動の問題が解消され、ミドルクリックでの開封にも対応します。
背景
#976 で導入された先行実装は、Ctrl/Cmd キーを押下した際にリンク要素に contenteditable=false を付与することでブラウザのネイティブなリンクナビゲーションに委ねる設計でした。この方式はホバー時のカーソル表示を含む視覚フィードバックとの整合性を取る上で有効でしたが、カーソル移動に問題を引き起こしていました。
問題の根本は contenteditable 属性のトグルという副作用の大きい操作にあります。data-links-openable 属性とCSS、そして Lexical のコマンドシステムを組み合わせることで、エディタのコンテンツに触れずにリンク開封を実現する設計に刷新されています。
技術的な変更
LinkOpenerExtension の register 関数が大幅に書き直され、contenteditable の操作を完全に排除した新しいアーキテクチャになりました。
変更前:
register: () => {
return mergeRegister(
registerEventListener(window, "keydown", this.#update.bind(this)),
registerEventListener(window, "keyup", this.#update.bind(this)),
registerEventListener(window, "blur", this.#disable.bind(this)),
registerEventListener(window, "focus", this.#refresh.bind(this))
)
}
変更後:
register: (editor) => mergeRegister(
editor.registerCommand(CLICK_COMMAND, this.#handleClick.bind(this), COMMAND_PRIORITY_NORMAL),
registerEventListener(this.editorElement.editorContentElement, "auxclick", this.#handleAuxClick.bind(this)),
registerEventListener(window, "keydown", this.#handleKey.bind(this)),
registerEventListener(window, "keyup", this.#handleKey.bind(this)),
registerEventListener(window, "focus", this.#handleFocus.bind(this))
)
クリックの処理は Lexical の CLICK_COMMAND で受け取り、$openLink でリンクを開くように変更されました。#handleClick はモディファイアキーが押されている場合のみ true を返してコマンドの伝播を止め、それ以外は false を返して通常のクリック処理に委ねます。また、window への blur イベントリスナーが削除され、代わりに focus ハンドラーが #handleFocus として整理されています。
カーソルスタイルの制御は CSS に委ねられるようになりました。エディタのルート要素に data-links-openable 属性が付与されている間、a 要素に cursor: pointer が適用されます。
&[data-links-openable] a {
cursor: pointer;
}
ミドルクリック対応は、エディタコンテンツ要素への auxclick イベントリスナーとして追加されています。src/helpers/timing_helpers.js には delay(ms) ユーティリティが追加されており、Promise ベースの setTimeout ラッパーとして非同期処理で活用されます。
テストも実装の変更に合わせて更新されています。contenteditable 属性や target・rel 属性の検証は廃止され、実際に新しいタブが開くか否かを context.waitForEvent("page") で確認するE2Eテストに置き換えられました。
設計判断
Lexical のコマンドシステムを活用する設計 が採用されました。contenteditable のトグルはブラウザのネイティブ動作に委ねるための手段でしたが、エディタのDOM構造を直接書き換える副作用を持ちます。CLICK_COMMAND を使うことで、クリックイベントの処理を Lexical のライフサイクル内で完結させ、エディタの状態管理との整合性を保っています。
カーソルスタイルと開封動作の関心分離も明確になっています。以前の実装では contenteditable=false がカーソル表示と開封動作を兼ねていましたが、新実装では data-links-openable 属性がカーソルスタイルのみを担い、開封は CLICK_COMMAND ハンドラーが担います。それぞれの責務が独立したため、どちらかの挙動を変更してももう一方に影響しにくくなっています。
まとめ
本PRは、エディタのDOM属性を副作用として操作するアプローチを廃止し、Lexical のコマンドシステムとCSSの役割分担に基づく設計へ移行した変更です。contenteditable のトグルという根本的な原因を取り除いたことで、カーソル移動の問題が解消されるとともに、ミドルクリック対応も自然な形で追加できる構造が整いました。