プレビュー不可の添付ファイルが壊れた画像として表示される問題を修正
Active Storageの添付ファイルで previewable="false" が正しく解釈されず壊れた画像として表示される問題と、パスワード保護されたPDFなどプレビュー生成に失敗するファイルでも同様の問題が発生していた状況を修正しました。
背景
DOMの getAttribute は属性値を常に文字列として返すため、previewable="false" は文字列 "false" として取得されます。JavaScriptでは空文字列でない文字列はすべてtruthyと評価されるため、"false" も true として扱われてしまいます。この挙動により、本来ファイルアイコンとファイル名で表示されるべき非プレビュー対象の添付ファイルが、画像として描画を試みて壊れたimgタグを生成していました。
また、previewable="true" であっても、パスワード保護されたPDFのようにActive Storageのrepresentation URLが500エラーを返す場合、<img> タグのsrcが取得できず同様に壊れた画像が表示されるケースも存在していました。この問題は previewable フラグの解釈とは独立した別の障害パスです。
技術的な変更
この修正は2つの独立したアプローチで構成されています。1つ目は previewable 属性の型変換の正規化、2つ目は画像読み込み失敗時のフォールバック処理の追加です。
parseBoolean 関数の追加により、文字列 "false" をboolean false として正しく評価できるようになりました。src/helpers/string_helper.js に追加されたこの関数は、値が文字列の場合は === "true" の比較でboolean変換を行い、それ以外は Boolean() に委譲します。
export function parseBoolean(value) {
if (typeof value === "string") return value === "true"
return Boolean(value)
}
ActionTextAttachmentNode のコンストラクタでは、this.previewable = previewable だったのを this.previewable = parseBoolean(previewable) に変更し、DOMから読み込まれた文字列値が確実にboolean型に変換されるようになりました。
onerror フォールバックは #createDOMForImage メソッドに追加されました。previewable がtrueかつ isPreviewableImage でない場合(PDFなどの非画像プレビュー対象)に限り、img.onerror ハンドラを設定します。
#createDOMForImage(options = {}) {
const img = createElement("img", { src: this.src, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options })
if (this.previewable && !this.isPreviewableImage) {
img.onerror = () => this.#swapPreviewToFileDOM(img)
}
const container = createElement("div", { className: "attachment__container" })
container.appendChild(img)
return container
}
#swapPreviewToFileDOM メソッドは、エラー発生時に figure 要素のCSSクラスを attachment--preview から attachment--file へ置き換え、preview用の attachment__container と figcaption を削除した上で、ファイルアイコンとファイル名の表示要素を追加します。重要なのは、このDOM操作はあくまで表示上の変換であり、ノードの previewable プロパティ自体は true のまま保持される点です。これにより exportDOM がサーバー向けに元の previewable 状態を正しく保持できます。
#swapPreviewToFileDOM(img) {
const figure = img.closest("figure.attachment")
if (!figure) return
figure.className = figure.className.replace("attachment--preview", "attachment--file")
const container = figure.querySelector(".attachment__container")
if (container) container.remove()
const caption = figure.querySelector("figcaption")
if (caption) caption.remove()
figure.appendChild(this.#createDOMForFile())
figure.appendChild(this.#createDOMForNotImage())
}
ブラウザテスト test/browser/tests/attachments/non_previewable_attachment.test.js が新たに追加され、previewable="false" でのファイル表示確認と、壊れたプレビューURLでの onerror フォールバック動作の両方が自動化されています。
設計判断
parseBoolean をヘルパー関数として共通化した判断は、同種の問題が他の属性でも発生しうることを踏まえた設計です。DOM属性の文字列→boolean変換は ActionTextAttachmentNode 固有の問題ではなく、汎用的なユーティリティとして string_helper.js に配置することで再利用可能にしています。
onerror フォールバックをノードのプロパティ変更なしにDOMレベルで処理する設計も注目に値します。previewable プロパティを false に書き換えると exportDOM の出力も変化してしまい、サーバー側の状態と不整合が生じます。DOM操作のみで見た目を変換することで、エディタ内部の状態とサーバー向けのシリアライズを切り離しています。
onerror ハンドラの適用条件 として !this.isPreviewableImage を設けている点も重要です。通常の画像ファイル(isPreviewableImage が true)に対してはハンドラを設定しないことで、通常画像の読み込み失敗を不必要にファイル表示に変換しないよう区別しています。
まとめ
DOMのstring/boolean境界を parseBoolean で明示的に正規化し、onerror によるDOMレベルのフォールバックでサーバー状態と表示状態を分離するという2層の対策により、この修正は型の曖昧さと外部依存の失敗という異なる根本原因をそれぞれ独立して解決しています。