mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-23 16:01:15 +00:00
fix(email): resolve layout overflow and blocked image empty spaces
Remove width: max-content and display: inline-block from email content CSS that caused horizontal scroll and left-side text clipping. Collapse empty table cells/containers when external images are blocked to prevent large blank areas in newsletter emails.
This commit is contained in:
+2
-12
@@ -88,9 +88,7 @@ body {
|
||||
|
||||
/* Enhanced Email Content Styling */
|
||||
|
||||
/* Wrapper uses inline-block to size to content, enabling horizontal scroll */
|
||||
.email-content-wrapper {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
@@ -100,16 +98,8 @@ body {
|
||||
line-height: 1.6;
|
||||
color: var(--color-foreground);
|
||||
max-width: none;
|
||||
/* Size to content's natural width, don't shrink below it */
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Prevent tables with width="100%" from shrinking below their content */
|
||||
.email-content table[width="100%"],
|
||||
.email-content table[style*="width:100%"],
|
||||
.email-content table[style*="width: 100%"] {
|
||||
min-width: max-content;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.email-content p {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Email } from "@/lib/jmap/types";
|
||||
import { hasRichFormatting, EMAIL_SANITIZE_CONFIG } from "@/lib/email-sanitization";
|
||||
import { hasRichFormatting, EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { formatFileSize, cn } from "@/lib/utils";
|
||||
@@ -466,11 +466,16 @@ export function EmailViewer({
|
||||
}
|
||||
|
||||
// Sanitize HTML to prevent XSS
|
||||
const cleanHtml = DOMPurify.sanitize(htmlContent, sanitizeConfig);
|
||||
let cleanHtml = DOMPurify.sanitize(htmlContent, sanitizeConfig);
|
||||
|
||||
// Remove the hook after sanitization
|
||||
DOMPurify.removeAllHooks();
|
||||
|
||||
// Collapse empty containers left behind by blocked images
|
||||
if (shouldBlockExternal && blockedExternalContent) {
|
||||
cleanHtml = collapseBlockedImageContainers(cleanHtml);
|
||||
}
|
||||
|
||||
// Update blocked content state
|
||||
if (blockedExternalContent && !hasBlockedContent) {
|
||||
setHasBlockedContent(true);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||
import { hasRichFormatting, EMAIL_SANITIZE_CONFIG } from "@/lib/email-sanitization";
|
||||
import { hasRichFormatting, EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { formatDate, formatFileSize, cn } from "@/lib/utils";
|
||||
@@ -296,11 +296,13 @@ function EmailCard({
|
||||
const sanitized = DOMPurify.sanitize(htmlContent, sanitizeConfig);
|
||||
DOMPurify.removeHook('afterSanitizeAttributes');
|
||||
|
||||
let finalHtml = sanitized;
|
||||
if (blockedExternalContent) {
|
||||
setHasBlockedContent(true);
|
||||
finalHtml = collapseBlockedImageContainers(sanitized);
|
||||
}
|
||||
|
||||
return { html: sanitized, isHtml: true };
|
||||
return { html: finalHtml, isHtml: true };
|
||||
}
|
||||
|
||||
// Plain text fallback
|
||||
|
||||
@@ -75,3 +75,35 @@ export function hasRichFormatting(html: string): boolean {
|
||||
'h1, h2, h3, h4, h5, h6, ul, ol, blockquote'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse empty containers left behind when external images are blocked.
|
||||
* Walks up from each blocked img to find the nearest table cell or wrapper div
|
||||
* and hides it if it contains no meaningful visible content.
|
||||
*/
|
||||
export function collapseBlockedImageContainers(html: string): string {
|
||||
const doc = parseHtmlSafely(html);
|
||||
const blockedImages = doc.querySelectorAll('img[data-blocked-src]');
|
||||
|
||||
blockedImages.forEach((img) => {
|
||||
let el: HTMLElement | null = img.parentElement;
|
||||
while (el && el !== doc.body) {
|
||||
if (el.tagName === 'TD' || el.tagName === 'TH' || (el.tagName === 'DIV' && el.parentElement?.tagName === 'TD')) {
|
||||
const hasVisibleText = el.textContent?.replace(/[\s\u00A0]+/g, '').trim();
|
||||
const hasVisibleMedia = el.querySelector('img:not([data-blocked-src]), video, canvas');
|
||||
const hasLinks = el.querySelector('a[href]');
|
||||
if (!hasVisibleText && !hasVisibleMedia && !hasLinks) {
|
||||
el.style.display = 'none';
|
||||
el.style.height = '0';
|
||||
el.style.padding = '0';
|
||||
el.style.overflow = 'hidden';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (el.tagName === 'TABLE' || el.tagName === 'TR') break;
|
||||
el = el.parentElement;
|
||||
}
|
||||
});
|
||||
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user