Skip to content

Commit bdf9483

Browse files
committed
fix: onerror when setting img placeholders
1 parent bb1edff commit bdf9483

3 files changed

Lines changed: 39 additions & 27 deletions

File tree

src/web/EnrichedText.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useMemo, type CSSProperties } from 'react';
1+
import { memo, useMemo, useRef, type CSSProperties } from 'react';
22
import type { EnrichedTextProps } from '../types';
33
import './EnrichedText.css';
44
import { enrichedTextStyleToCSSProperties } from './styleConversion/enrichedTextStyleToCSSProperties';
@@ -9,9 +9,12 @@ import { buildMentionRulesCSS } from './styleConversion/buildMentionRulesCSS';
99
import { sanitizeHtml } from './sanitization/htmlSanitizer';
1010
import { prepareHtmlForWeb } from './normalization/prepareHtmlForWeb';
1111
import { INLINE_IMAGE_CSS_VARIABLES } from './styleConversion/inlineImageCSSVariables';
12+
import { useImageErrorFallback } from './useImageErrorFallback';
1213

1314
export const EnrichedText = memo(
1415
({ children, htmlStyle, style, selectionColor }: EnrichedTextProps) => {
16+
const containerRef = useRef<HTMLDivElement>(null);
17+
1518
const sanitizedHtml = useMemo(() => sanitizeHtml(children), [children]);
1619

1720
const finalHtml = useMemo(
@@ -52,10 +55,13 @@ export const EnrichedText = memo(
5255
[textStyle, themingStyle, cssVars]
5356
);
5457

58+
useImageErrorFallback(containerRef, finalHtml);
59+
5560
return (
5661
<>
5762
<style>{mentionRulesCSS}</style>
5863
<div
64+
ref={containerRef}
5965
style={finalStyle}
6066
className={ENRICHED_TEXT_CLASSNAME}
6167
dangerouslySetInnerHTML={{ __html: finalHtml }}

src/web/normalization/prepareHtmlForWeb.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,5 @@
11
export function prepareHtmlForWeb(html: string) {
2-
return addImageErrorFallback(checkboxHtmlToWeb(html));
3-
}
4-
5-
/*
6-
* Flag images that fail to load so CSS can swap in a broken-image
7-
* placeholder (see `img.error` in EnrichedText.css).
8-
*
9-
* The markup is rendered via dangerouslySetInnerHTML, where an `onerror`
10-
* attribute on an <img> still fires on load failure. This runs AFTER
11-
* sanitization on purpose, so the handler we add isn't stripped.
12-
*/
13-
function addImageErrorFallback(html: string): string {
14-
const parser = new DOMParser();
15-
const doc = parser.parseFromString(html, 'text/html');
16-
17-
doc.querySelectorAll('img').forEach((img) => {
18-
const src = img.getAttribute('src');
19-
20-
if (!src || src.trim() === '') {
21-
img.classList.add('error');
22-
}
23-
24-
img.setAttribute('onerror', "this.classList.add('error')");
25-
});
26-
27-
return doc.body.innerHTML;
2+
return checkboxHtmlToWeb(html);
283
}
294

305
/*

src/web/useImageErrorFallback.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect, type RefObject } from 'react';
2+
3+
/*
4+
* Flag images that fail to load so CSS can swap in a broken-image placeholder.
5+
*/
6+
export const useImageErrorFallback = (
7+
containerRef: RefObject<HTMLElement | null>,
8+
html: string
9+
) => {
10+
useEffect(() => {
11+
const container = containerRef.current;
12+
if (!container) return;
13+
14+
const images = container.querySelectorAll<HTMLImageElement>('img');
15+
const cleanups: (() => void)[] = [];
16+
17+
images.forEach((img) => {
18+
const handleImageError = () => img.classList.add('error');
19+
20+
img.addEventListener('error', handleImageError);
21+
cleanups.push(() => img.removeEventListener('error', handleImageError));
22+
23+
// Catch images that already failed before the listener attached
24+
if (img.complete && img.naturalHeight === 0) {
25+
handleImageError();
26+
}
27+
});
28+
29+
return () => cleanups.forEach((cleanup) => cleanup());
30+
}, [containerRef, html]);
31+
};

0 commit comments

Comments
 (0)