email-builder-utils 1.1.49 → 1.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/blocks/button.d.ts +0 -27
- package/dist/utils/blocks/button.d.ts.map +1 -1
- package/dist/utils/blocks/button.js +89 -39
- package/dist/utils/blocks/dividers.js +30 -30
- package/dist/utils/blocks/grid.d.ts.map +1 -1
- package/dist/utils/blocks/grid.js +192 -106
- package/dist/utils/blocks/image.d.ts.map +1 -1
- package/dist/utils/blocks/image.js +42 -4
- package/dist/utils/blocks/shape.js +89 -89
- package/dist/utils/blocks/text.d.ts.map +1 -1
- package/dist/utils/blocks/text.js +80 -49
- package/dist/utils/blocks/video.js +56 -56
- package/dist/utils/buildStyles.d.ts.map +1 -1
- package/dist/utils/buildStyles.js +24 -6
- package/dist/utils/common.d.ts +1 -0
- package/dist/utils/common.d.ts.map +1 -1
- package/dist/utils/common.js +14 -0
- package/dist/utils/convertJsonToHtml.d.ts.map +1 -1
- package/dist/utils/convertJsonToHtml.js +183 -153
- package/dist/utils/outlookSupport.d.ts.map +1 -1
- package/dist/utils/outlookSupport.js +81 -57
- package/package.json +34 -34
|
@@ -6,18 +6,22 @@ const buildStyles_1 = require("../buildStyles");
|
|
|
6
6
|
const gradientUtils_1 = require("../gradientUtils");
|
|
7
7
|
const common_1 = require("../common");
|
|
8
8
|
const jsonToHTML_1 = require("../jsonToHTML");
|
|
9
|
-
const outlookSupport_1 = require("../outlookSupport");
|
|
10
9
|
async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
11
10
|
const { style = {}, childrenIds = [], props } = blockData.data;
|
|
12
11
|
const { columns = 1, cellWidths = [], responsive = true } = props;
|
|
13
12
|
const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
|
|
14
13
|
const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
14
|
+
// Detect gradient — check both backgroundImage prop and customCss (gradient may land in
|
|
15
|
+
// customCss when the block was built via CSS shorthand or custom CSS input).
|
|
15
16
|
const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : '';
|
|
16
17
|
const customCssStr = restStyle.customCss || '';
|
|
18
|
+
// Extract gradient string from customCss if not already in backgroundImage
|
|
17
19
|
const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
|
|
18
20
|
? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
|
|
19
21
|
: '';
|
|
20
|
-
const effectiveGradient = bgImageStr.includes('gradient(')
|
|
22
|
+
const effectiveGradient = bgImageStr.includes('gradient(')
|
|
23
|
+
? bgImageStr
|
|
24
|
+
: gradientInCustomCss;
|
|
21
25
|
const isGradient = Boolean(effectiveGradient);
|
|
22
26
|
const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
|
|
23
27
|
const fallbackBgColor = backgroundColor ||
|
|
@@ -27,12 +31,18 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
27
31
|
const rawBgImageUrl = !isGradient && bgImageStr
|
|
28
32
|
? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
|
|
29
33
|
: null;
|
|
34
|
+
// When gradient came from customCss, strip background-image from customCss so it
|
|
35
|
+
// doesn't duplicate into the inner table style (the outer <td> wrapper carries it).
|
|
30
36
|
const innerCustomCss = gradientInCustomCss
|
|
31
37
|
? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
|
|
32
38
|
: customCssStr;
|
|
39
|
+
// Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
|
|
40
|
+
// background props from the inner table so the outer <td> background shows through.
|
|
33
41
|
const innerRestStyleRaw = (rawBgImageUrl || isGradient)
|
|
34
42
|
? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
|
|
35
43
|
: { ...restStyle, customCss: innerCustomCss };
|
|
44
|
+
// Extract border/radius props — applied via a div wrapper for non-MSO clients so that
|
|
45
|
+
// border-radius is honoured (Gmail/Outlook compose strip border-radius from <table>).
|
|
36
46
|
const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
|
|
37
47
|
const divBorderParts = [];
|
|
38
48
|
if (borderRadius)
|
|
@@ -51,36 +61,52 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
51
61
|
? 'transparent'
|
|
52
62
|
: ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
|
|
53
63
|
const tableStyles = (0, buildStyles_1.buildStyles)({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
|
|
54
|
-
perChanges: [],
|
|
64
|
+
perChanges: [],
|
|
65
|
+
pxChanges: buildStyles_1.allPxAttributes,
|
|
55
66
|
});
|
|
56
67
|
const total = childrenIds.length;
|
|
57
68
|
const visualRows = Math.ceil(total / columns);
|
|
69
|
+
// OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
|
|
58
70
|
const msoTableWidth = Math.min(cellWidthInPx, 600);
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
// When a background image/gradient is present, the background is applied on an outer
|
|
72
|
+
// wrapper <td> (see bottom of function). The inner grid tables must be clean.
|
|
73
|
+
// When no background, the MSO table gets bgcolor for solid-color sections.
|
|
74
|
+
const msoBgColor = !rawBgImageUrl && !isGradient
|
|
75
|
+
? (backgroundColor || '')
|
|
76
|
+
: '';
|
|
77
|
+
const msoBgAttr = (0, common_1.buildOutlookBgAttr)(msoBgColor);
|
|
61
78
|
const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
|
|
62
|
-
|
|
63
|
-
const
|
|
79
|
+
// Inner tables must be explicitly transparent when outer <td> carries the background.
|
|
80
|
+
const innerBgTransparent = (rawBgImageUrl || isGradient)
|
|
81
|
+
? 'background-color:transparent;'
|
|
82
|
+
: '';
|
|
83
|
+
const nonMsoBgAttr = !rawBgImageUrl && !isGradient && !divBorderStyle
|
|
84
|
+
? (0, common_1.buildOutlookBgAttr)(backgroundColor || '')
|
|
85
|
+
: '';
|
|
86
|
+
// When divBorderStyle is set the non-MSO <table> is transparent, so the Grid's
|
|
87
|
+
// backgroundColor must move onto the div wrapper — otherwise it vanishes in modern clients.
|
|
88
|
+
// Skip this for bg-image/gradient blocks; they apply their background via a separate wrapper.
|
|
64
89
|
const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
|
|
65
90
|
? ` background-color:${backgroundColor};`
|
|
66
91
|
: '';
|
|
67
92
|
const divWrapOpen = divBorderStyle ? `<div style="${divBorderStyle}${divWrapBg}">` : '';
|
|
68
93
|
const divWrapClose = divBorderStyle ? `</div>` : '';
|
|
69
|
-
let html = `
|
|
70
|
-
<!--[if mso]>
|
|
71
|
-
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
|
|
72
|
-
style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
|
|
73
|
-
class="${gridVisibilityClass}">
|
|
74
|
-
<![endif]-->
|
|
75
|
-
<!--[if !mso]><!-->
|
|
76
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%"
|
|
77
|
-
role="presentation"${nonMsoBgAttr}
|
|
78
|
-
style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
|
|
79
|
-
class="${gridVisibilityClass}">
|
|
80
|
-
<!--<![endif]-->
|
|
94
|
+
let html = `
|
|
95
|
+
<!--[if mso]>
|
|
96
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
|
|
97
|
+
style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
|
|
98
|
+
class="${gridVisibilityClass}">
|
|
99
|
+
<![endif]-->
|
|
100
|
+
<!--[if !mso]><!-->
|
|
101
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" align="center"
|
|
102
|
+
role="presentation"${nonMsoBgAttr}
|
|
103
|
+
style="border-collapse:collapse; table-layout:fixed; ${innerBgTransparent}${tableStyles}; max-width:600px;"
|
|
104
|
+
class="${gridVisibilityClass}">
|
|
105
|
+
<!--<![endif]-->
|
|
81
106
|
`;
|
|
82
107
|
for (let r = 0; r < visualRows; r++) {
|
|
83
108
|
html += "<tr>";
|
|
109
|
+
// COUNT visible cells and find last visible column index
|
|
84
110
|
let visibleCells = 0;
|
|
85
111
|
let lastVisibleCol = 0;
|
|
86
112
|
const rowIds = [];
|
|
@@ -89,12 +115,14 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
89
115
|
const id = childrenIds[idx] ?? null;
|
|
90
116
|
rowIds.push(id);
|
|
91
117
|
const child = id ? rootData[id] : null;
|
|
92
|
-
|
|
118
|
+
const isHidden = child?.data?.props?.hideOnDesktop;
|
|
119
|
+
if (!isHidden) {
|
|
93
120
|
visibleCells++;
|
|
94
121
|
lastVisibleCol = c;
|
|
95
122
|
}
|
|
96
123
|
}
|
|
97
124
|
const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
|
|
125
|
+
// Reserve pixel space for spacer tds between visible cells (N-1 gaps for N visible cells)
|
|
98
126
|
const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
|
|
99
127
|
const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
|
|
100
128
|
let totalWidth = 0;
|
|
@@ -102,19 +130,24 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
102
130
|
for (let c = 0; c < columns; c++) {
|
|
103
131
|
const id = rowIds[c];
|
|
104
132
|
let widthPercent = cellWidths[c] ?? safeWidth;
|
|
105
|
-
if (widthPercent <= 0 || widthPercent > 100)
|
|
133
|
+
if (widthPercent <= 0 || widthPercent > 100) {
|
|
106
134
|
widthPercent = safeWidth;
|
|
135
|
+
}
|
|
107
136
|
cellWidthPercents.push(widthPercent);
|
|
108
137
|
if (id) {
|
|
109
138
|
const child = rootData[id];
|
|
110
|
-
|
|
139
|
+
const isHidden = child?.data?.props?.hideOnDesktop;
|
|
140
|
+
if (!isHidden) {
|
|
111
141
|
totalWidth += widthPercent;
|
|
142
|
+
}
|
|
112
143
|
}
|
|
113
144
|
}
|
|
114
145
|
const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
|
|
115
146
|
for (let c = 0; c < columns; c++) {
|
|
116
147
|
const id = rowIds[c];
|
|
117
|
-
let widthPercent =
|
|
148
|
+
let widthPercent = cellWidthPercents[c] * scaleFactor;
|
|
149
|
+
widthPercent = Math.min(widthPercent, 100);
|
|
150
|
+
// Cell pixel width is a share of the gap-adjusted table width
|
|
118
151
|
const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
|
|
119
152
|
if (id) {
|
|
120
153
|
const child = rootData[id];
|
|
@@ -124,42 +157,73 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
124
157
|
const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
|
|
125
158
|
if (childVisible) {
|
|
126
159
|
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
|
|
160
|
+
// bgcolor on the cell <td> for Old Outlook (Word engine).
|
|
161
|
+
// When the cell has no border-radius and the grid has no border, use the grid's
|
|
162
|
+
// backgroundColor as the fallback so the grid background shows through empty cells.
|
|
163
|
+
// Skipped for border-radius cells — bgcolor is rectangular and would bleed through
|
|
164
|
+
// the div's rounded-corner clip in CSS-capable clients (corner squares).
|
|
127
165
|
const cellBgColor = cellStyle.backgroundColor || '';
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
${
|
|
166
|
+
const cellHasBorderRadius = Boolean(cellStyle.borderRadius);
|
|
167
|
+
const canApplyGridBgFallback = !cellHasBorderRadius && !divBorderStyle;
|
|
168
|
+
const effectiveCellBg = cellBgColor || (canApplyGridBgFallback ? msoBgColor : '');
|
|
169
|
+
const cellBgAttr = (0, common_1.buildOutlookBgAttr)(effectiveCellBg);
|
|
170
|
+
html += `
|
|
171
|
+
<td
|
|
172
|
+
width="${Math.max(1, Math.round(widthPercent))}%"${cellBgAttr}
|
|
173
|
+
class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
|
|
174
|
+
style="width:${widthPercent}%;vertical-align:${verticalAlign};word-break:break-word;${styles}"
|
|
175
|
+
>
|
|
176
|
+
${childHtml}
|
|
136
177
|
</td>`;
|
|
178
|
+
// Spacer td between columns — uses width attribute only (no inline style width) so
|
|
179
|
+
// Outlook mobile (which strips <style>) treats it proportionally, not as a fixed blocker.
|
|
180
|
+
// col-gap-spacer class hides it when columns stack via CSS media query.
|
|
137
181
|
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
138
|
-
|
|
182
|
+
const gapBgAttr = divBorderStyle ? '' : (0, common_1.buildOutlookBgAttr)(msoBgColor);
|
|
183
|
+
html += `<td width="${columnGap}"${gapBgAttr} class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
139
184
|
}
|
|
140
185
|
}
|
|
141
186
|
}
|
|
142
187
|
else {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
188
|
+
const emptyBgAttr = divBorderStyle ? '' : (0, common_1.buildOutlookBgAttr)(msoBgColor);
|
|
189
|
+
html += `
|
|
190
|
+
<td width="${Math.max(1, Math.round(widthPercent))}%"
|
|
191
|
+
${emptyBgAttr}
|
|
192
|
+
${responsive ? 'class="stack-column"' : ""}
|
|
193
|
+
style="width:${widthPercent}%;vertical-align:top;">
|
|
147
194
|
</td>`;
|
|
148
195
|
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
149
|
-
|
|
196
|
+
const gapBgAttr = divBorderStyle ? '' : (0, common_1.buildOutlookBgAttr)(msoBgColor);
|
|
197
|
+
html += `<td width="${columnGap}"${gapBgAttr} class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
150
198
|
}
|
|
151
199
|
}
|
|
152
200
|
}
|
|
153
201
|
html += "</tr>";
|
|
154
202
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
203
|
+
// Close both MSO and non-MSO tables
|
|
204
|
+
html += `
|
|
205
|
+
<!--[if mso]>
|
|
206
|
+
</table>
|
|
207
|
+
<![endif]-->
|
|
208
|
+
<!--[if !mso]><!-->
|
|
209
|
+
</table>
|
|
210
|
+
<!--<![endif]-->
|
|
162
211
|
`;
|
|
212
|
+
// ── Background image: canonical multi-client approach ────────────────────
|
|
213
|
+
//
|
|
214
|
+
// Problem: `background-image` on a <table> element is stripped by:
|
|
215
|
+
// • New Outlook Mac / Windows (Chromium-based app)
|
|
216
|
+
// • Outlook.com
|
|
217
|
+
// • Old Outlook (Word engine) — ignores CSS entirely
|
|
218
|
+
//
|
|
219
|
+
// Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
|
|
220
|
+
// the background. Different clients pick it up via different mechanisms:
|
|
221
|
+
//
|
|
222
|
+
// background="" attribute on <td> → Yahoo Mail, older webmail
|
|
223
|
+
// CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
|
|
224
|
+
// VML v:rect inside the <td> → Old Outlook (Word engine) ✓
|
|
225
|
+
//
|
|
226
|
+
// The inner grid tables have NO background so the outer <td> bg shows through.
|
|
163
227
|
if (rawBgImageUrl || isGradient) {
|
|
164
228
|
const vmlFill = isGradient
|
|
165
229
|
? (() => {
|
|
@@ -169,89 +233,99 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
169
233
|
return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
|
|
170
234
|
})()
|
|
171
235
|
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
172
|
-
html = `
|
|
173
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
|
|
174
|
-
style="border-collapse:collapse;width:100%;max-width:${msoTableWidth}px;">
|
|
175
|
-
<tr>
|
|
176
|
-
<td width="100%"
|
|
177
|
-
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
|
|
178
|
-
style="
|
|
179
|
-
width:100%;max-width:${msoTableWidth}px;
|
|
180
|
-
background-color:${fallbackBgColor};
|
|
181
|
-
${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
|
|
182
|
-
">
|
|
183
|
-
|
|
184
|
-
<!--[if gte mso 9]>
|
|
185
|
-
<v:rect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
186
|
-
fill="true" stroke="false"
|
|
187
|
-
style="width:${msoTableWidth}px;">
|
|
188
|
-
${vmlFill}
|
|
189
|
-
<v:textbox inset="0,0,0,0">
|
|
190
|
-
<![endif]-->
|
|
191
|
-
|
|
192
|
-
${html}
|
|
193
|
-
|
|
194
|
-
<!--[if gte mso 9]>
|
|
195
|
-
</v:textbox>
|
|
196
|
-
</v:rect>
|
|
197
|
-
<![endif]-->
|
|
198
|
-
|
|
199
|
-
</td>
|
|
200
|
-
</tr>
|
|
236
|
+
html = `
|
|
237
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
|
|
238
|
+
style="border-collapse:collapse;table-layout:fixed;width:100%;max-width:${msoTableWidth}px;">
|
|
239
|
+
<tr>
|
|
240
|
+
<td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
|
|
241
|
+
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
|
|
242
|
+
style="
|
|
243
|
+
width:100%;max-width:${msoTableWidth}px;
|
|
244
|
+
background-color:${fallbackBgColor};
|
|
245
|
+
${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
|
|
246
|
+
">
|
|
247
|
+
|
|
248
|
+
<!--[if gte mso 9]>
|
|
249
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
250
|
+
fill="true" stroke="false"
|
|
251
|
+
style="width:${msoTableWidth}px;">
|
|
252
|
+
${vmlFill}
|
|
253
|
+
<v:textbox inset="0,0,0,0">
|
|
254
|
+
<![endif]-->
|
|
255
|
+
|
|
256
|
+
${html}
|
|
257
|
+
|
|
258
|
+
<!--[if gte mso 9]>
|
|
259
|
+
</v:textbox>
|
|
260
|
+
</v:rect>
|
|
261
|
+
<![endif]-->
|
|
262
|
+
|
|
263
|
+
</td>
|
|
264
|
+
</tr>
|
|
201
265
|
</table>`;
|
|
202
266
|
}
|
|
267
|
+
// Wrap the entire grid (including any bg-image outer table) in a div when the block
|
|
268
|
+
// has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
|
|
269
|
+
// — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
|
|
270
|
+
// border-radius on <div> but still shows the rectangular border; new Outlook works fully.
|
|
203
271
|
if (divBorderStyle)
|
|
204
272
|
html = `${divWrapOpen}${html}${divWrapClose}`;
|
|
205
273
|
return html;
|
|
206
274
|
}
|
|
207
275
|
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
|
|
208
276
|
const { style = {}, childrenIds = [], props = {} } = blockData.data;
|
|
277
|
+
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
278
|
+
// Extract border + radius from style so they move to the div wrapper (not the <td>).
|
|
279
|
+
// Gmail strips border-radius from <td> but honours it on <div>. By putting border and
|
|
280
|
+
// radius on the same unconditional <div>, the rounded card border renders in all clients.
|
|
281
|
+
// The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
|
|
209
282
|
const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
|
|
283
|
+
// backgroundColor must stay on the div wrapper (not the <td>) in two cases:
|
|
284
|
+
// 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
|
|
285
|
+
// 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
|
|
286
|
+
// In both cases, the rectangular <td> background bleeds through rounded corners if kept
|
|
287
|
+
// in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
|
|
210
288
|
const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
|
|
211
|
-
//
|
|
212
|
-
// background-color:transparent can defeat the bgcolor HTML attribute in
|
|
213
|
-
// (Word engine), leaving the cell with no background at all.
|
|
289
|
+
// When stripping bg from <td>, omit backgroundColor entirely rather than setting 'transparent'.
|
|
290
|
+
// An explicit background-color:transparent in CSS can defeat the bgcolor HTML attribute in
|
|
291
|
+
// Old Outlook (Word engine), leaving the cell with no background at all.
|
|
214
292
|
const styleForTd = stripBgFromTd
|
|
215
293
|
? { ...styleWithoutBorder, backgroundColor: undefined }
|
|
216
294
|
: styleWithoutBorder;
|
|
217
|
-
|
|
295
|
+
// Outlook treats <td width="..."> as content width and then adds horizontal padding,
|
|
296
|
+
// which can push the total row width beyond the parent table and shift columns.
|
|
297
|
+
// Keep sizing styles on <td>, but move padding into the inner div wrapper.
|
|
298
|
+
const styleForTdWithoutPadding = { ...styleForTd, padding: undefined };
|
|
299
|
+
const styles = (0, buildStyles_1.buildStyles)(styleForTdWithoutPadding, {
|
|
300
|
+
perChanges: [],
|
|
301
|
+
pxChanges: buildStyles_1.allPxAttributes,
|
|
302
|
+
});
|
|
218
303
|
const parts = [];
|
|
304
|
+
// OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
|
|
305
|
+
// If parent is 600px and cell is 50%, cell width should be 300px, not 600px
|
|
219
306
|
const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
else if (typeof rawPad === 'string') {
|
|
228
|
-
const parts = rawPad.trim().split(/\s+/).map(v => parseFloat(v) || 0);
|
|
229
|
-
if (parts.length >= 4) {
|
|
230
|
-
cellPadRight = parts[1];
|
|
231
|
-
cellPadLeft = parts[3];
|
|
232
|
-
}
|
|
233
|
-
else if (parts.length === 3) {
|
|
234
|
-
cellPadRight = parts[1];
|
|
235
|
-
cellPadLeft = parts[1];
|
|
236
|
-
}
|
|
237
|
-
else if (parts.length === 2) {
|
|
238
|
-
cellPadRight = parts[1];
|
|
239
|
-
cellPadLeft = parts[1];
|
|
240
|
-
}
|
|
241
|
-
else if (parts.length === 1) {
|
|
242
|
-
cellPadRight = parts[0];
|
|
243
|
-
cellPadLeft = parts[0];
|
|
244
|
-
}
|
|
245
|
-
}
|
|
307
|
+
// Subtract the cell's own padding so children receive the actual content-area width.
|
|
308
|
+
// Old Outlook honours explicit img/table width attributes — if a child is sized to the
|
|
309
|
+
// full column width (ignoring padding) it overflows and expands the column.
|
|
310
|
+
const cellPad = styleWithoutBorder?.padding || {};
|
|
311
|
+
const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
|
|
312
|
+
const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
|
|
246
313
|
const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
|
|
314
|
+
// OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
|
|
247
315
|
const safeCellWidthPx = Math.min(contentWidthPx, 600);
|
|
248
316
|
for (const childId of childrenIds) {
|
|
249
317
|
const child = rootData[childId];
|
|
250
|
-
if (child)
|
|
318
|
+
if (child) {
|
|
251
319
|
parts.push(await (0, jsonToHTML_1.convertToHtml)(child, rootData, safeCellWidthPx));
|
|
320
|
+
}
|
|
252
321
|
}
|
|
253
322
|
const borderRadius = cellBorderRadius || 0;
|
|
254
323
|
const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
|
|
324
|
+
// Build border CSS for the div wrapper.
|
|
325
|
+
// When the parent grid already has a divBorderStyle wrapper (border + border-radius +
|
|
326
|
+
// overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
|
|
327
|
+
// two concentric borders of the same colour (double-border). The grid's wrapper div
|
|
328
|
+
// already provides the visual container; the cell div only needs background-color.
|
|
255
329
|
const cellDivBorderParts = [];
|
|
256
330
|
if (!parentGridHasBorder) {
|
|
257
331
|
if (borderRadius)
|
|
@@ -267,9 +341,21 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
267
341
|
}
|
|
268
342
|
}
|
|
269
343
|
const cellDivBorderStyle = cellDivBorderParts.join(' ');
|
|
344
|
+
// Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
|
|
345
|
+
// background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
|
|
270
346
|
const divStyleParts = [`background-color:${bgColor};`];
|
|
347
|
+
const formatPad = (value) => (typeof value === 'number' ? `${value}px` : (value || '0'));
|
|
348
|
+
const cellPadTop = formatPad(cellPad.top);
|
|
349
|
+
const cellPadBottom = formatPad(cellPad.bottom);
|
|
350
|
+
const cellPadLeftCss = formatPad(cellPad.left);
|
|
351
|
+
const cellPadRightCss = formatPad(cellPad.right);
|
|
352
|
+
divStyleParts.push(`padding:${cellPadTop} ${cellPadRightCss} ${cellPadBottom} ${cellPadLeftCss};`);
|
|
271
353
|
if (cellDivBorderStyle)
|
|
272
354
|
divStyleParts.push(cellDivBorderStyle);
|
|
273
|
-
const
|
|
274
|
-
|
|
355
|
+
const divStyleStr = divStyleParts.join(' ');
|
|
356
|
+
const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
|
|
357
|
+
return {
|
|
358
|
+
html: wrapped,
|
|
359
|
+
styles,
|
|
360
|
+
};
|
|
275
361
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/image.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/image.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM;;;;;GAoB5B;AAED,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAiH5E"}
|
|
@@ -29,27 +29,65 @@ async function convertImageBlock(blockData, cellWidthInPx) {
|
|
|
29
29
|
const { altText, imageUrl, navigateToUrl } = props;
|
|
30
30
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
31
31
|
const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
|
|
32
|
-
|
|
32
|
+
// Add border styles to container for fallback clients
|
|
33
|
+
const containerStyles = (0, buildStyles_1.buildStyles)({
|
|
34
|
+
...containerStyle,
|
|
35
|
+
}, { perChanges: [], pxChanges: buildStyles_1.addPxToAttributes });
|
|
36
|
+
// OUTLOOK FIX: Ensure cellWidthInPx never exceeds 600px
|
|
33
37
|
const safeCellWidth = Math.min(cellWidthInPx, 600);
|
|
38
|
+
// Parse width percentage (default 100%)
|
|
34
39
|
const widthPercent = typeof width === "string" && width.includes("%")
|
|
35
40
|
? parseInt(width.replace("%", ""))
|
|
36
|
-
: typeof width === "number"
|
|
41
|
+
: typeof width === "number"
|
|
42
|
+
? width
|
|
43
|
+
: 100;
|
|
44
|
+
// OUTLOOK FIX: Calculate inner container width based on safe cell width
|
|
37
45
|
const paddingLeft = style?.padding?.left || 0;
|
|
38
46
|
const paddingRight = style?.padding?.right || 0;
|
|
39
47
|
const availableWidth = safeCellWidth - paddingLeft - paddingRight;
|
|
40
48
|
const innerContainerWidth = Math.round((widthPercent / 100) * availableWidth);
|
|
49
|
+
// Get image dimensions and calculate scaled sizes
|
|
41
50
|
const { originalWidth, originalHeight, scaledWidth, scaledHeight } = await computeScaledDimensions(imageUrl, innerContainerWidth);
|
|
51
|
+
// OUTLOOK FIX: For Outlook, we need exact pixel dimensions
|
|
52
|
+
// Calculate final dimensions that respect both original size and container
|
|
42
53
|
const finalWidth = Math.min(scaledWidth, innerContainerWidth, originalWidth);
|
|
43
54
|
const finalHeight = Math.round((finalWidth / originalWidth) * originalHeight);
|
|
44
|
-
|
|
55
|
+
// Build image styles for modern email clients (non-Outlook)
|
|
56
|
+
const imageTagStyles = (0, buildStyles_1.buildStyles)({
|
|
57
|
+
borderStyle,
|
|
58
|
+
borderRadius: borderRadius,
|
|
59
|
+
borderColor,
|
|
60
|
+
borderWidth,
|
|
61
|
+
}, {
|
|
62
|
+
perChanges: [],
|
|
63
|
+
pxChanges: buildStyles_1.addPxToAttributes,
|
|
64
|
+
});
|
|
65
|
+
// OUTLOOK RENDERING CONTRACT
|
|
66
|
+
// width/height HTML attributes → Old Outlook Word engine (CSS is lower priority in Word)
|
|
67
|
+
// width/height inline CSS → New Outlook, OWA, and all CSS-capable clients
|
|
68
|
+
// max-width:100% → prevents overflow in any client that ignores pixel constraints
|
|
69
|
+
// height:auto !important → @media query (convertJsonToHtml.ts) overrides this on mobile
|
|
70
|
+
// so proportional scaling is preserved on small screens
|
|
71
|
+
// -ms-interpolation-mode → inline fallback; the <style> block is stripped by
|
|
72
|
+
// Outlook during forward/reply MIME rewriting
|
|
45
73
|
const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; width:${finalWidth}px; height:${finalHeight}px; max-width:100%; line-height:0; -ms-interpolation-mode:bicubic;" />`;
|
|
46
74
|
const percentWidth = typeof width === "string" && width.endsWith("%")
|
|
47
75
|
? width
|
|
48
|
-
: typeof width === "number"
|
|
76
|
+
: typeof width === "number"
|
|
77
|
+
? `${width}%`
|
|
78
|
+
: "100%";
|
|
79
|
+
// Non-MSO wrapper: display:block removes the phantom inline-baseline gap that
|
|
80
|
+
// display:inline-block creates in Gmail / Apple Mail / Yahoo between images.
|
|
81
|
+
// margin handles alignment since text-align won't move block elements.
|
|
49
82
|
const imgTextAlign = containerStyle.textAlign || "left";
|
|
50
83
|
const imgMargin = imgTextAlign === "center" ? "margin:0 auto;" :
|
|
51
84
|
imgTextAlign === "right" ? "margin-left:auto; margin-right:0;" : "";
|
|
85
|
+
// OUTLOOK FIX: Use finalWidth (the actual displayed size) as max-width so the div
|
|
86
|
+
// doesn't claim more space than the image occupies. originalWidth is the natural
|
|
87
|
+
// image size (e.g. 636px for the Beefree logo rendered at 35px) which was
|
|
88
|
+
// misleadingly large and could confuse some rendering engines.
|
|
52
89
|
const nonMsoWrapper = `<div style="display:block; width:${percentWidth}; max-width:${finalWidth}px; line-height:0; font-size:0; ${imgMargin}">${imageElement}</div>`;
|
|
90
|
+
// OUTLOOK FIX: Generate VML with corrected dimensions
|
|
53
91
|
const outlookImage = await (0, outlookSupport_1.appendOutlookForImage)(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
|
|
54
92
|
const imageContent = (0, outlookSupport_1.appendOutlookSupport)(outlookImage, containerStyles, visibilityClass, safeCellWidth);
|
|
55
93
|
return navigateToUrl
|