再接続時に拡張機能のツールバーボタンが重複する問題を修正

basecamp/lexxy

エディタが再接続されるたびに拡張機能のツールバーボタンが追加され続けるバグを修正しました。initializeToolbars() にボタンの重複排除ロジックを組み込み、拡張機能ごとの対処なしにフレームワーク側で冪等性を保証します。

背景

Lexxyのエディタは再接続(例: 下書きコメントへの再訪問)のたびに initializeToolbar を各拡張機能に対して呼び出します。この設計の中に重複追加の原因が潜んでいました。

ツールバーのDOMは再接続をまたいで持続しますが、拡張機能のインスタンスは再接続のたびに新規作成されます。外部のボタンセレクタで既存ボタンを参照できる拡張機能とは異なり、voiceNote: true のようなboolean値で設定された拡張機能は自前でボタンを生成します。新規インスタンスは以前に挿入したボタンを記憶していないため、再接続のたびにボタンをDOMに追加し続けました。

この問題への対処として、個々の拡張機能が重複チェックを実装するワークアラウンドも検討されましたが(bc3リポジトリの#10261)、本PRはフレームワーク側での根本解決を採用しています。

技術的な変更

src/editor/extensions.jsinitializeToolbars() が、ツールバーボタンのクリーンアップと追加を担う2つのプライベートメソッドに分割されました。

変更前:

initializeToolbars() {
  if (this.#lexxyToolbar) {
    this.enabledExtensions.forEach(ext => ext.initializeToolbar(this.#lexxyToolbar))
  }
}

変更後:

initializeToolbars() {
  const toolbar = this.#lexxyToolbar
  if (!toolbar) return

  this.#clearPreviousExtensionToolbarButtons(toolbar)
  this.#addExtensionToolbarButtons(toolbar)
}

#clearPreviousExtensionToolbarButtons(toolbar) {
  toolbar.querySelectorAll("[data-lexxy-extension]").forEach(el => el.remove())
}

#addExtensionToolbarButtons(toolbar) {
  this.enabledExtensions.forEach(ext => {
    const childrenBefore = new Set(toolbar.children)
    ext.initializeToolbar(toolbar)
    for (const child of toolbar.children) {
      if (!childrenBefore.has(child)) {
        child.setAttribute("data-lexxy-extension", "")
      }
    }
  })
}

仕組みの核心は data-lexxy-extension 属性による追跡です。#addExtensionToolbarButtons は各拡張機能の initializeToolbar 呼び出し前後でツールバーの子要素をスナップショット(Set)として記録し、新たに追加された要素に data-lexxy-extension 属性を付与します。次回の initializeToolbars() 呼び出し時、#clearPreviousExtensionToolbarButtons がこの属性を持つ要素をすべて削除してから再追加を行います。

ブラウザテストも追加されています。test/browser/fixtures/extension-toolbar-button.htmltest/browser/fixtures/extension-toolbar-button.jsToolbarButtonExtension を定義し、test/browser/tests/editor/reconnect.test.js ではエディタのDOMを親から切り離して再挿入することで再接続を再現し、custom-extension-button の数が常に1であることを検証しています。

設計判断

ツールバーの冪等性をフレームワーク側で保証する方針が採用されました。

各拡張機能に重複チェックのロジックを持たせる方法も選択肢でしたが、それでは拡張機能の実装者が再接続ライフサイクルを意識し続ける必要が生じます。data-lexxy-extension 属性によるマーキングを initializeToolbars() が透過的に行うことで、拡張機能側のAPIである initializeToolbar(toolbar) は変更なしに既存コードとの互換性を保っています。

また、ext.initializeToolbar 呼び出しの前後でツールバーの子要素を Set で差分検出する手法は、拡張機能がどの位置にボタンを挿入してもマーキングが機能する汎用的な実装です。拡張機能が insertAdjacentElement を使うケースや複数要素を追加するケースも自然に処理できます。

まとめ

この変更は、再接続ライフサイクルに起因するDOM重複という問題を、data-lexxy-extension 属性という軽量なマーキング機構でフレームワーク側に封じ込めた例です。個々の拡張機能が防御的なコードを持つ必要がなくなり、拡張機能の実装を単純に保ったままエディタの堅牢性を高められます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
f37397ac

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「総論→各論→結論」の構成が明確です。リード文で要旨を述べ、背景、技術的な変更、設計判断で詳細を掘り下げ、最後にまとめで意義を述べるという理想的な構成になっています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(`javascript:src/editor/extensions.js`)およびGitHubのPRリンク記法(`[#10261](URL)`)が正しく使用されています。

対象読者への適合性 ✓ PASS

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

エディタの再接続ライフサイクルやDOM操作など、専門知識を持つエンジニアを対象とした内容になっており、過度な説明がなく簡潔です。

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

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

各セクション、各パラグラフがトピックセンテンスで始まり、要点が掴みやすい構成です。1段落1トピックの原則も守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

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

`src/editor/extensions.js` の変更前後のコードがDiffと完全に一致しています。また、テストコードに関する説明もDiffで追加されたファイルと内容が整合しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「冪等性」「DOM」「インスタンス」などの技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

`data-lexxy-extension` 属性によるマーキングと削除の仕組み、および `Set` を用いた差分検出の解説が、実際のコード変更と一致しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのタイトル、Description、Diffの内容で裏付けられています。特に、代替案として言及されている `bc3` リポジトリのPRへの言及も、PR Descriptionに基づいた正確な情報です。

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

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

PR番号 `#958` および関連PR番号 `#10261` が正確に記載されています。

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

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

記事のタイトル「再接続時に拡張機能のツールバーボタンが重複する問題を修正」は、PRのタイトル「Prevent extension toolbar buttons from duplicating on reconnect」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

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

PR情報に記載のない、バージョンサポート状況やリリース日程などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

PR情報と矛盾するような時間表現の歪曲は見られません。