`turbo-permanent`要素内のエディタをTurboキャッシュリセットから除外
Turboのページ遷移時に[data-turbo-permanent]コンテナ内のLexxyエディタが誤ってリセットされ、操作不能になるバグが修正されました。3行の条件分岐追加により、turbo:before-cacheハンドラは永続要素内のエディタをスキップするようになります。
背景
Turboの[data-turbo-permanent]要素はページスナップショットから除外され、ナビゲーション中もDOMに残り続けます。この仕組みにより、サイドバーや永続的なUIコンポーネントはページ遷移をまたいで状態を保持できます。
しかし、Lexxyの #handleTurboBeforeCache ハンドラはこの区別を考慮せず、ページ内のすべてのエディタに対して #reset() を呼び出していました。#reset()は内部のcontenteditabledivを取り除く処理であり、通常はconnectedCallbackが再実行されてエディタが再構築されます。[data-turbo-permanent]要素はDOMから切り離されも再接続もされないため、connectedCallbackが再び発火することがなく、エディタは永続的に操作不能な状態になります。
Basecampでは、この問題がサイドバーのpingテキスト入力欄として具体的に現れていました。カード編集モードに入り「Never Mind」をクリックしてページ遷移が発生すると、サイドバーのエディタが消失し、フルページリフレッシュなしには復元できなくなります。
技術的な変更
src/elements/editor.jsの#handleTurboBeforeCacheメソッドに、closest("[data-turbo-permanent]")を使った条件分岐が追加されました。
変更前:
#handleTurboBeforeCache = (event) => {
this.#reset()
}
変更後:
#handleTurboBeforeCache = (event) => {
if (!this.closest("[data-turbo-permanent]")) {
this.#reset()
}
}
closest() はDOM APIのメソッドで、要素自身およびその祖先ツリーを遡り、セレクタに一致する最初の要素を返します。エディタ自体またはその親要素のいずれかにdata-turbo-permanent属性があれば#reset()はスキップされます。これにより、[data-turbo-permanent]外のエディタは従来通りリセットされ、Turboのページスナップショットに正しく対応し続けます。
設計判断
closest()によるDOM探索 を採用したことで、エディタ自身が[data-turbo-permanent]属性を持たなくても、祖先要素に属性があれば適切に除外されます。これにより、エディタコンポーネント自体に永続性の情報を持たせる必要がなく、HTMLマークアップのみで制御できます。
また、エディタ側にTurboの永続要素の概念を明示的に登録する機構を設けるのではなく、既存のTurboのセマンティクス([data-turbo-permanent])をそのまま照合する設計になっています。Turboがスナップショットから除外する要素と、Lexxyがリセットをスキップする要素が同一の属性で定義されるため、両者の振る舞いが自然に一致します。
まとめ
この修正は、Turboの永続要素のライフサイクルとLexxyのリセット処理の前提が噛み合っていなかった問題を、Turbo自身のセマンティクスに準拠した3行の変更で解決しています。connectedCallbackが再実行されない要素に対してリセットを行わないという原則を明示することで、同種の問題が今後発生するリスクも低減されます。