`contenteditable=false` を廃止し、CLICK_COMMANDでリンク開封を実装

basecamp/lexxy

Ctrl/Cmd+クリックによるリンク開封の実装を、contenteditable=false のトグルから Lexical の CLICK_COMMAND ベースに置き換えました。これによりカーソル移動の問題が解消され、ミドルクリックでの開封にも対応します。

背景

#976 で導入された先行実装は、Ctrl/Cmd キーを押下した際にリンク要素に contenteditable=false を付与することでブラウザのネイティブなリンクナビゲーションに委ねる設計でした。この方式はホバー時のカーソル表示を含む視覚フィードバックとの整合性を取る上で有効でしたが、カーソル移動に問題を引き起こしていました。

問題の根本は contenteditable 属性のトグルという副作用の大きい操作にあります。data-links-openable 属性とCSS、そして Lexical のコマンドシステムを組み合わせることで、エディタのコンテンツに触れずにリンク開封を実現する設計に刷新されています。

技術的な変更

LinkOpenerExtensionregister 関数が大幅に書き直され、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 属性や targetrel 属性の検証は廃止され、実際に新しいタブが開くか否かを context.waitForEvent("page") で確認するE2Eテストに置き換えられました。

設計判断

Lexical のコマンドシステムを活用する設計 が採用されました。contenteditable のトグルはブラウザのネイティブ動作に委ねるための手段でしたが、エディタのDOM構造を直接書き換える副作用を持ちます。CLICK_COMMAND を使うことで、クリックイベントの処理を Lexical のライフサイクル内で完結させ、エディタの状態管理との整合性を保っています。

カーソルスタイルと開封動作の関心分離も明確になっています。以前の実装では contenteditable=false がカーソル表示と開封動作を兼ねていましたが、新実装では data-links-openable 属性がカーソルスタイルのみを担い、開封は CLICK_COMMAND ハンドラーが担います。それぞれの責務が独立したため、どちらかの挙動を変更してももう一方に影響しにくくなっています。

まとめ

本PRは、エディタのDOM属性を副作用として操作するアプローチを廃止し、Lexical のコマンドシステムとCSSの役割分担に基づく設計へ移行した変更です。contenteditable のトグルという根本的な原因を取り除いたことで、カーソル移動の問題が解消されるとともに、ミドルクリック対応も自然な形で追加できる構造が整いました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
0bc540d2

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)」という理想的な3部構成が明確に守られています。各セクションの役割が明確で、非常に理解しやすい構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(`javascript:path`)、PR番号のリンク記法(`[#976](URL)`)など、カスタムMarkdown構文がすべて正しく使用されています。

対象読者への適合性 ✓ PASS

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

「Lexical」「CLICK_COMMAND」「contenteditable」などの専門用語を前提として解説しており、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクションの冒頭で要点が述べられ、各段落の1文目がトピックセンテンスとして機能しています。1段落1トピックの原則も守られており、可読性が非常に高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロック(`link_opener_extension.js`の変更前後、`lexxy-editor.css`の追加部分)は、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

PRで言及されている「CLICK_COMMAND」や「auxclick」などの技術用語を正確に使用し、適切に解説しています。用語の誤用は見られません。

説明の技術的正確性 ✓ PASS

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

`contenteditable`のトグルに起因するカーソル問題を`CLICK_COMMAND`で解決したという説明は、PRの趣旨とDiffの内容に完全に一致しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張(先行実装の問題点、`CLICK_COMMAND`への移行、ミドルクリック対応など)は、PRのDescriptionやDiffの内容によって裏付けられています。ハルシネーションは検出されませんでした。

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

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

言及されているPR番号(#998, #976)は正確です。

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

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

記事のタイトル「`contenteditable=false` を廃止し、CLICK_COMMANDでリンク開封を実装」は、PRのタイトル「Ctrl/cmd+click to open links without `contenteditable=false`」の内容を的確に要約しており、整合性が取れています。

外部知識の正確性 ✓ PASS

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

記事の内容はPR情報とDiffに限定されており、バージョンサポートやリリース予定など、PRに記載のない外部知識の追記は見られませんでした。

時間表現の正確性 ✓ PASS

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

「先行実装」や現在の変更内容といった時間的な前後関係が、PRの文脈通りに正確に表現されています。時間表現の歪曲はありません。