email-builder-utils 1.1.49 → 1.1.50
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 +70 -20
- package/dist/utils/blocks/grid.d.ts.map +1 -1
- package/dist/utils/blocks/grid.js +131 -55
- package/dist/utils/blocks/image.d.ts.map +1 -1
- package/dist/utils/blocks/image.js +42 -4
- package/dist/utils/blocks/shape.js +9 -9
- package/dist/utils/blocks/text.d.ts.map +1 -1
- package/dist/utils/blocks/text.js +57 -26
- 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 +31 -1
- package/dist/utils/outlookSupport.d.ts.map +1 -1
- package/dist/utils/outlookSupport.js +24 -7
- package/package.json +1 -1
|
@@ -1,29 +1,2 @@
|
|
|
1
|
-
export declare function appendOutlookForButton(buttonData: {
|
|
2
|
-
style: any;
|
|
3
|
-
text: string;
|
|
4
|
-
navigateToUrl: string;
|
|
5
|
-
}): {
|
|
6
|
-
innerContent: string;
|
|
7
|
-
computed: {
|
|
8
|
-
fs: any;
|
|
9
|
-
fontWeight: any;
|
|
10
|
-
containerAlign: any;
|
|
11
|
-
padTop: any;
|
|
12
|
-
padRight: any;
|
|
13
|
-
padBottom: any;
|
|
14
|
-
padLeft: any;
|
|
15
|
-
explicitWidth: any;
|
|
16
|
-
vmlWidth: number;
|
|
17
|
-
safeColor: any;
|
|
18
|
-
bgColor: any;
|
|
19
|
-
safeFF: string;
|
|
20
|
-
finalHeight: any;
|
|
21
|
-
explicitHeight: any;
|
|
22
|
-
bw: any;
|
|
23
|
-
br: any;
|
|
24
|
-
bdColor: any;
|
|
25
|
-
bdStyle: any;
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
1
|
export declare function convertButtonBlock(blockData: any): string;
|
|
29
2
|
//# sourceMappingURL=button.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/button.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/button.ts"],"names":[],"mappings":"AA6IA,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,GAAG,UAuFhD"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.appendOutlookForButton = appendOutlookForButton;
|
|
4
3
|
exports.convertButtonBlock = convertButtonBlock;
|
|
5
4
|
const buildStyles_1 = require("../buildStyles");
|
|
6
5
|
const fontFallback_1 = require("../fontFallback");
|
|
@@ -26,27 +25,55 @@ function appendOutlookForButton(buttonData) {
|
|
|
26
25
|
const fontWeight = style.fontWeight || 400;
|
|
27
26
|
const containerAlign = style.alignment || style.textAlign || "left";
|
|
28
27
|
const explicitWidth = typeof style.width === "number" && style.width > 0 ? style.width : 0;
|
|
28
|
+
// VML requires explicit pixel width — estimate from text + padding when not set.
|
|
29
|
+
// Slightly conservative multiplier to reduce clipping risk in Outlook.
|
|
29
30
|
const estimatedTextWidth = Math.ceil((text || "").length * fs * 0.62);
|
|
30
31
|
const vmlWidthBase = explicitWidth || Math.max(120, estimatedTextWidth + padLeft + padRight + bw * 2);
|
|
32
|
+
// For pill buttons (large radius), ensure enough width so the curved ends don't encroach.
|
|
31
33
|
const minPillWidth = br > 0 ? Math.ceil(finalHeight * 2) : 0;
|
|
32
34
|
const vmlWidth = Math.max(vmlWidthBase, minPillWidth);
|
|
33
35
|
const borderCss = bw > 0 ? `border:${bw}px ${bdStyle} ${bdColor};` : "";
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Empty href triggers New Outlook's link sanitizer to strip surrounding styles.
|
|
37
|
+
const safeHref = navigateToUrl && navigateToUrl.trim() ? navigateToUrl : "#";
|
|
38
|
+
// ── Non-MSO fallback ──
|
|
39
|
+
// All visual box styling (bg, border, radius, width, height) lives on the <td>.
|
|
40
|
+
// New Outlook (and many web mail clients) strip these from <a> inline styles,
|
|
41
|
+
// treating anchors as inline text. The <td> is a block container and is preserved.
|
|
42
|
+
// The <a> fills the cell so the entire button is the click target — typography only.
|
|
43
|
+
// Down-level-hidden conditional comments hide this branch from classic Outlook
|
|
44
|
+
// (which renders the VML below), so both engines see exactly one button.
|
|
45
|
+
const tdWidthCss = explicitWidth ? `width:${explicitWidth}px;` : "";
|
|
46
|
+
const tdHeightCss = explicitHeight > 0 ? `height:${finalHeight}px;` : "";
|
|
47
|
+
const tdWidthAttr = explicitWidth ? ` width="${explicitWidth}"` : "";
|
|
48
|
+
const tdHeightAttr = explicitHeight > 0 ? ` height="${finalHeight}"` : "";
|
|
49
|
+
// <a> fills the cell. When height is explicit, line-height = inner height for vertical
|
|
50
|
+
// centering. When not, padding provides the vertical sizing (matches canvas behavior).
|
|
51
|
+
const anchorBoxStyles = explicitHeight > 0
|
|
52
|
+
? `display:block;line-height:${finalHeight - 2 * bw}px;padding:0 ${padRight}px;`
|
|
53
|
+
: `display:block;padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;line-height:${fs}px;`;
|
|
54
|
+
const tableAlignAttr = ` align="${containerAlign}"`;
|
|
55
|
+
const tableMargin = containerAlign === 'center' ? 'margin:0 auto;'
|
|
56
|
+
: containerAlign === 'right' ? 'margin-left:auto;margin-right:0;'
|
|
57
|
+
: '';
|
|
38
58
|
const nonMsoAnchor = `<!--[if !mso]><!-->
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation"${tableAlignAttr} style="border-collapse:separate;${tableMargin}">
|
|
60
|
+
<tr>
|
|
61
|
+
<td bgcolor="${bgColor}" align="center" valign="middle"${tdWidthAttr}${tdHeightAttr} style="background-color:${bgColor};border-radius:${br}px;${borderCss}${tdWidthCss}${tdHeightCss}box-sizing:border-box;mso-padding-alt:0;text-align:center;">
|
|
62
|
+
<a href="${safeHref}" target="_blank" rel="noreferrer noopener"
|
|
63
|
+
style="${anchorBoxStyles}color:${safeColor};font-family:${safeFF};font-size:${fs}px;font-weight:${fontWeight};text-decoration:none;text-align:center;white-space:nowrap;-webkit-text-size-adjust:none;box-sizing:border-box;">${text}</a>
|
|
64
|
+
</td>
|
|
65
|
+
</tr>
|
|
66
|
+
</table>
|
|
42
67
|
<!--<![endif]-->`;
|
|
68
|
+
// ── MSO: VML bulletproof button (classic Outlook / Word engine) ──
|
|
69
|
+
// VML arcsize is a percentage of half the shorter side. Clamp to 50% (pill).
|
|
43
70
|
const arcSizePct = br > 0 ? Math.min(Math.round((br / (finalHeight / 2)) * 100), 50) : 0;
|
|
44
71
|
const strokeAttrs = bw > 0
|
|
45
72
|
? `stroke="true" strokecolor="${bdColor}" strokeweight="${bw}px"`
|
|
46
73
|
: `stroke="false"`;
|
|
47
74
|
const msoButton = `<!--[if mso]>
|
|
48
75
|
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
|
|
49
|
-
href="${
|
|
76
|
+
href="${safeHref}"
|
|
50
77
|
style="height:${finalHeight}px;v-text-anchor:middle;width:${vmlWidth}px;"
|
|
51
78
|
arcsize="${arcSizePct}%"
|
|
52
79
|
${strokeAttrs}
|
|
@@ -63,12 +90,24 @@ function appendOutlookForButton(buttonData) {
|
|
|
63
90
|
return {
|
|
64
91
|
innerContent,
|
|
65
92
|
computed: {
|
|
66
|
-
fs,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
fs,
|
|
94
|
+
fontWeight,
|
|
95
|
+
containerAlign,
|
|
96
|
+
padTop,
|
|
97
|
+
padRight,
|
|
98
|
+
padBottom,
|
|
99
|
+
padLeft,
|
|
100
|
+
explicitWidth,
|
|
101
|
+
vmlWidth,
|
|
102
|
+
safeColor,
|
|
103
|
+
bgColor,
|
|
104
|
+
safeFF,
|
|
105
|
+
finalHeight,
|
|
106
|
+
explicitHeight,
|
|
107
|
+
bw,
|
|
108
|
+
br,
|
|
109
|
+
bdColor,
|
|
110
|
+
bdStyle,
|
|
72
111
|
},
|
|
73
112
|
};
|
|
74
113
|
}
|
|
@@ -79,9 +118,20 @@ function convertButtonBlock(blockData) {
|
|
|
79
118
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
80
119
|
const { innerContent, computed } = appendOutlookForButton({
|
|
81
120
|
style: {
|
|
82
|
-
fontFamily,
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
fontFamily,
|
|
122
|
+
fontSize,
|
|
123
|
+
fontWeight,
|
|
124
|
+
textAlign,
|
|
125
|
+
borderColor,
|
|
126
|
+
borderRadius,
|
|
127
|
+
borderWidth,
|
|
128
|
+
borderStyle,
|
|
129
|
+
buttonPadding,
|
|
130
|
+
color,
|
|
131
|
+
buttonColor,
|
|
132
|
+
width,
|
|
133
|
+
height,
|
|
134
|
+
alignment,
|
|
85
135
|
},
|
|
86
136
|
text: text || "",
|
|
87
137
|
navigateToUrl: navigateToUrl || "",
|
|
@@ -118,9 +168,9 @@ function convertButtonBlock(blockData) {
|
|
|
118
168
|
hideOnMobile: Boolean(props.hideOnMobile),
|
|
119
169
|
});
|
|
120
170
|
return `
|
|
121
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" class="${visibilityClass}" data-block-type="button" data-block-props="${buttonBlockProps}">
|
|
171
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" class="${visibilityClass}" data-block-type="button" data-block-props="${buttonBlockProps}" style="border-collapse:collapse;table-layout:fixed;">
|
|
122
172
|
<tr>
|
|
123
|
-
<td align="${computed.containerAlign}"
|
|
173
|
+
<td align="${computed.containerAlign}"${(0, common_1.buildOutlookBgAttr)(containerBg)}
|
|
124
174
|
style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;background-color:${containerBg || 'transparent'};">
|
|
125
175
|
${innerContent}
|
|
126
176
|
</td>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/grid.ts"],"names":[],"mappings":"AAMA,wBAAsB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/grid.ts"],"names":[],"mappings":"AAMA,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAgTtB;AAED,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,GAAG,EACb,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,EAAE,MAAM,EACzB,mBAAmB,UAAQ;;;GAwG5B"}
|
|
@@ -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,16 +61,31 @@ 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
|
: '';
|
|
@@ -73,14 +98,15 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
73
98
|
class="${gridVisibilityClass}">
|
|
74
99
|
<![endif]-->
|
|
75
100
|
<!--[if !mso]><!-->
|
|
76
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%"
|
|
101
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" align="center"
|
|
77
102
|
role="presentation"${nonMsoBgAttr}
|
|
78
|
-
style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
|
|
103
|
+
style="border-collapse:collapse; table-layout:fixed; ${innerBgTransparent}${tableStyles}; max-width:600px;"
|
|
79
104
|
class="${gridVisibilityClass}">
|
|
80
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,34 +157,40 @@ 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> ensures background-color survives Outlook
|
|
161
|
+
// compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
|
|
127
162
|
const cellBgColor = cellStyle.backgroundColor || '';
|
|
128
|
-
const cellBgAttr =
|
|
163
|
+
const cellBgAttr = (0, common_1.buildOutlookBgAttr)(cellBgColor);
|
|
129
164
|
html += `
|
|
130
165
|
<td
|
|
131
|
-
width="${
|
|
166
|
+
width="${Math.max(1, Math.round(widthPercent))}%"${cellBgAttr}
|
|
132
167
|
class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
|
|
133
|
-
style="width:${
|
|
168
|
+
style="width:${widthPercent}%;vertical-align:${verticalAlign};word-break:break-word;${styles}"
|
|
134
169
|
>
|
|
135
170
|
${childHtml}
|
|
136
171
|
</td>`;
|
|
172
|
+
// Spacer td between columns — uses width attribute only (no inline style width) so
|
|
173
|
+
// Outlook mobile (which strips <style>) treats it proportionally, not as a fixed blocker.
|
|
174
|
+
// col-gap-spacer class hides it when columns stack via CSS media query.
|
|
137
175
|
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
138
|
-
html += `<td width="${columnGap}" style="
|
|
176
|
+
html += `<td width="${columnGap}" class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
139
177
|
}
|
|
140
178
|
}
|
|
141
179
|
}
|
|
142
180
|
else {
|
|
143
181
|
html += `
|
|
144
|
-
<td width="${
|
|
182
|
+
<td width="${Math.max(1, Math.round(widthPercent))}%"
|
|
145
183
|
${responsive ? 'class="stack-column"' : ""}
|
|
146
|
-
style="width:${
|
|
184
|
+
style="width:${widthPercent}%;vertical-align:top;">
|
|
147
185
|
</td>`;
|
|
148
186
|
if (columnGap > 0 && c !== lastVisibleCol) {
|
|
149
|
-
html += `<td width="${columnGap}" style="
|
|
187
|
+
html += `<td width="${columnGap}" class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
|
|
150
188
|
}
|
|
151
189
|
}
|
|
152
190
|
}
|
|
153
191
|
html += "</tr>";
|
|
154
192
|
}
|
|
193
|
+
// Close both MSO and non-MSO tables
|
|
155
194
|
html += `
|
|
156
195
|
<!--[if mso]>
|
|
157
196
|
</table>
|
|
@@ -160,6 +199,21 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
160
199
|
</table>
|
|
161
200
|
<!--<![endif]-->
|
|
162
201
|
`;
|
|
202
|
+
// ── Background image: canonical multi-client approach ────────────────────
|
|
203
|
+
//
|
|
204
|
+
// Problem: `background-image` on a <table> element is stripped by:
|
|
205
|
+
// • New Outlook Mac / Windows (Chromium-based app)
|
|
206
|
+
// • Outlook.com
|
|
207
|
+
// • Old Outlook (Word engine) — ignores CSS entirely
|
|
208
|
+
//
|
|
209
|
+
// Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
|
|
210
|
+
// the background. Different clients pick it up via different mechanisms:
|
|
211
|
+
//
|
|
212
|
+
// background="" attribute on <td> → Yahoo Mail, older webmail
|
|
213
|
+
// CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
|
|
214
|
+
// VML v:rect inside the <td> → Old Outlook (Word engine) ✓
|
|
215
|
+
//
|
|
216
|
+
// The inner grid tables have NO background so the outer <td> bg shows through.
|
|
163
217
|
if (rawBgImageUrl || isGradient) {
|
|
164
218
|
const vmlFill = isGradient
|
|
165
219
|
? (() => {
|
|
@@ -171,9 +225,9 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
171
225
|
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
172
226
|
html = `
|
|
173
227
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
|
|
174
|
-
style="border-collapse:collapse;width:100%;max-width:${msoTableWidth}px;">
|
|
228
|
+
style="border-collapse:collapse;table-layout:fixed;width:100%;max-width:${msoTableWidth}px;">
|
|
175
229
|
<tr>
|
|
176
|
-
<td width="100%"
|
|
230
|
+
<td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
|
|
177
231
|
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
|
|
178
232
|
style="
|
|
179
233
|
width:100%;max-width:${msoTableWidth}px;
|
|
@@ -200,58 +254,68 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
200
254
|
</tr>
|
|
201
255
|
</table>`;
|
|
202
256
|
}
|
|
257
|
+
// Wrap the entire grid (including any bg-image outer table) in a div when the block
|
|
258
|
+
// has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
|
|
259
|
+
// — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
|
|
260
|
+
// border-radius on <div> but still shows the rectangular border; new Outlook works fully.
|
|
203
261
|
if (divBorderStyle)
|
|
204
262
|
html = `${divWrapOpen}${html}${divWrapClose}`;
|
|
205
263
|
return html;
|
|
206
264
|
}
|
|
207
265
|
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
|
|
208
266
|
const { style = {}, childrenIds = [], props = {} } = blockData.data;
|
|
267
|
+
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
268
|
+
// Extract border + radius from style so they move to the div wrapper (not the <td>).
|
|
269
|
+
// Gmail strips border-radius from <td> but honours it on <div>. By putting border and
|
|
270
|
+
// radius on the same unconditional <div>, the rounded card border renders in all clients.
|
|
271
|
+
// The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
|
|
209
272
|
const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
|
|
273
|
+
// backgroundColor must stay on the div wrapper (not the <td>) in two cases:
|
|
274
|
+
// 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
|
|
275
|
+
// 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
|
|
276
|
+
// In both cases, the rectangular <td> background bleeds through rounded corners if kept
|
|
277
|
+
// in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
|
|
210
278
|
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.
|
|
279
|
+
// When stripping bg from <td>, omit backgroundColor entirely rather than setting 'transparent'.
|
|
280
|
+
// An explicit background-color:transparent in CSS can defeat the bgcolor HTML attribute in
|
|
281
|
+
// Old Outlook (Word engine), leaving the cell with no background at all.
|
|
214
282
|
const styleForTd = stripBgFromTd
|
|
215
283
|
? { ...styleWithoutBorder, backgroundColor: undefined }
|
|
216
284
|
: styleWithoutBorder;
|
|
217
|
-
|
|
285
|
+
// Outlook treats <td width="..."> as content width and then adds horizontal padding,
|
|
286
|
+
// which can push the total row width beyond the parent table and shift columns.
|
|
287
|
+
// Keep sizing styles on <td>, but move padding into the inner div wrapper.
|
|
288
|
+
const styleForTdWithoutPadding = { ...styleForTd, padding: undefined };
|
|
289
|
+
const styles = (0, buildStyles_1.buildStyles)(styleForTdWithoutPadding, {
|
|
290
|
+
perChanges: [],
|
|
291
|
+
pxChanges: buildStyles_1.allPxAttributes,
|
|
292
|
+
});
|
|
218
293
|
const parts = [];
|
|
294
|
+
// OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
|
|
295
|
+
// If parent is 600px and cell is 50%, cell width should be 300px, not 600px
|
|
219
296
|
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
|
-
}
|
|
297
|
+
// Subtract the cell's own padding so children receive the actual content-area width.
|
|
298
|
+
// Old Outlook honours explicit img/table width attributes — if a child is sized to the
|
|
299
|
+
// full column width (ignoring padding) it overflows and expands the column.
|
|
300
|
+
const cellPad = styleWithoutBorder?.padding || {};
|
|
301
|
+
const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
|
|
302
|
+
const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
|
|
246
303
|
const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
|
|
304
|
+
// OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
|
|
247
305
|
const safeCellWidthPx = Math.min(contentWidthPx, 600);
|
|
248
306
|
for (const childId of childrenIds) {
|
|
249
307
|
const child = rootData[childId];
|
|
250
|
-
if (child)
|
|
308
|
+
if (child) {
|
|
251
309
|
parts.push(await (0, jsonToHTML_1.convertToHtml)(child, rootData, safeCellWidthPx));
|
|
310
|
+
}
|
|
252
311
|
}
|
|
253
312
|
const borderRadius = cellBorderRadius || 0;
|
|
254
313
|
const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
|
|
314
|
+
// Build border CSS for the div wrapper.
|
|
315
|
+
// When the parent grid already has a divBorderStyle wrapper (border + border-radius +
|
|
316
|
+
// overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
|
|
317
|
+
// two concentric borders of the same colour (double-border). The grid's wrapper div
|
|
318
|
+
// already provides the visual container; the cell div only needs background-color.
|
|
255
319
|
const cellDivBorderParts = [];
|
|
256
320
|
if (!parentGridHasBorder) {
|
|
257
321
|
if (borderRadius)
|
|
@@ -267,9 +331,21 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
267
331
|
}
|
|
268
332
|
}
|
|
269
333
|
const cellDivBorderStyle = cellDivBorderParts.join(' ');
|
|
334
|
+
// Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
|
|
335
|
+
// background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
|
|
270
336
|
const divStyleParts = [`background-color:${bgColor};`];
|
|
337
|
+
const formatPad = (value) => (typeof value === 'number' ? `${value}px` : (value || '0'));
|
|
338
|
+
const cellPadTop = formatPad(cellPad.top);
|
|
339
|
+
const cellPadBottom = formatPad(cellPad.bottom);
|
|
340
|
+
const cellPadLeftCss = formatPad(cellPad.left);
|
|
341
|
+
const cellPadRightCss = formatPad(cellPad.right);
|
|
342
|
+
divStyleParts.push(`padding:${cellPadTop} ${cellPadRightCss} ${cellPadBottom} ${cellPadLeftCss};`);
|
|
271
343
|
if (cellDivBorderStyle)
|
|
272
344
|
divStyleParts.push(cellDivBorderStyle);
|
|
273
|
-
const
|
|
274
|
-
|
|
345
|
+
const divStyleStr = divStyleParts.join(' ');
|
|
346
|
+
const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
|
|
347
|
+
return {
|
|
348
|
+
html: wrapped,
|
|
349
|
+
styles,
|
|
350
|
+
};
|
|
275
351
|
}
|
|
@@ -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
|
|
@@ -147,7 +147,7 @@ async function convertShapeBlock(blockData) {
|
|
|
147
147
|
// --- Case 1: Image + Text ---
|
|
148
148
|
if (imageUrl && text) {
|
|
149
149
|
nonMsoContent = `
|
|
150
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
150
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
|
|
151
151
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
152
152
|
border-radius:${resolvedBorderRadius};
|
|
153
153
|
background-color:${finalBackgroundColor};
|
|
@@ -156,11 +156,11 @@ async function convertShapeBlock(blockData) {
|
|
|
156
156
|
background-size:cover;
|
|
157
157
|
background-repeat:no-repeat;
|
|
158
158
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
159
|
-
<table border="0" cellpadding="0" cellspacing="0" width="
|
|
160
|
-
style="width
|
|
159
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%"
|
|
160
|
+
style="width:100%;height:${resolvedHeightPx}px;border-collapse:collapse;">
|
|
161
161
|
<tr>
|
|
162
162
|
<td align="${textAlignStyle}" valign="${verticalAlign}"
|
|
163
|
-
|
|
163
|
+
height="${resolvedHeightPx}"
|
|
164
164
|
style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
|
|
165
165
|
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
|
|
166
166
|
</td>
|
|
@@ -171,7 +171,7 @@ async function convertShapeBlock(blockData) {
|
|
|
171
171
|
// --- Case 2: Image only ---
|
|
172
172
|
else if (imageUrl) {
|
|
173
173
|
nonMsoContent = `
|
|
174
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
174
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
|
|
175
175
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
176
176
|
border-radius:${resolvedBorderRadius};
|
|
177
177
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
@@ -183,16 +183,16 @@ async function convertShapeBlock(blockData) {
|
|
|
183
183
|
// --- Case 3: Text only ---
|
|
184
184
|
else {
|
|
185
185
|
nonMsoContent = `
|
|
186
|
-
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
186
|
+
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
|
|
187
187
|
background-color:${finalBackgroundColor};
|
|
188
188
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
189
189
|
border-radius:${resolvedBorderRadius};
|
|
190
190
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
191
|
-
<table border="0" cellpadding="0" cellspacing="0" width="
|
|
192
|
-
style="width
|
|
191
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%"
|
|
192
|
+
style="width:100%;height:${resolvedHeightPx}px;border-collapse:collapse;">
|
|
193
193
|
<tr>
|
|
194
194
|
<td align="${textAlignStyle}" valign="${verticalAlign}"
|
|
195
|
-
|
|
195
|
+
height="${resolvedHeightPx}"
|
|
196
196
|
style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
|
|
197
197
|
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
|
|
198
198
|
</td>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/text.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/text.ts"],"names":[],"mappings":"AASA,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,UAkLtE"}
|
|
@@ -8,42 +8,69 @@ const common_1 = require("../common");
|
|
|
8
8
|
function convertTextBlock(blockData, cellWidthInPx) {
|
|
9
9
|
const { style, props } = blockData.data;
|
|
10
10
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
11
|
-
const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
11
|
+
const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
|
|
12
|
+
...rest } = style;
|
|
13
|
+
// Detect background image or gradient (may live in backgroundImage or customCss).
|
|
14
|
+
// Same multi-client approach as Grid block: outer wrapper <td> carries the background
|
|
15
|
+
// via CSS (Gmail/New Outlook), background attribute (Yahoo), and VML (Old Outlook).
|
|
16
|
+
const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : "";
|
|
17
|
+
const customCssStr = rest.customCss || "";
|
|
18
|
+
const gradientInCustomCss = !bgImageStr.includes("gradient(") && customCssStr.includes("gradient(")
|
|
19
|
+
? customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || ""
|
|
20
|
+
: "";
|
|
21
|
+
const effectiveGradient = bgImageStr.includes("gradient(")
|
|
22
|
+
? bgImageStr
|
|
23
|
+
: gradientInCustomCss;
|
|
18
24
|
const isGradient = Boolean(effectiveGradient);
|
|
19
25
|
const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
|
|
20
26
|
const rawBgImageUrl = !isGradient && bgImageStr
|
|
21
|
-
? bgImageStr.replace(/^url\(['"]?/,
|
|
27
|
+
? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
|
|
22
28
|
: null;
|
|
23
29
|
const hasBgImage = Boolean(rawBgImageUrl || isGradient);
|
|
24
30
|
const fallbackBgColor = textContainerBackgroundColor ||
|
|
25
31
|
parsedGradient?.fallback ||
|
|
26
32
|
(0, gradientUtils_1.extractCssFallbackColor)(customCssStr) ||
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
33
|
+
"#ffffff";
|
|
34
|
+
// Text box decoration styles (border, background, padding) — no width
|
|
35
|
+
const textBoxStyle = {
|
|
36
|
+
backgroundColor,
|
|
37
|
+
padding,
|
|
38
|
+
borderRadius,
|
|
39
|
+
borderStyle,
|
|
40
|
+
borderColor,
|
|
41
|
+
borderWidth,
|
|
42
|
+
};
|
|
43
|
+
const convertedTextStyle = (0, buildStyles_1.buildStyles)(textBoxStyle, {
|
|
44
|
+
perChanges: [],
|
|
45
|
+
pxChanges: buildStyles_1.allPxAttributes,
|
|
46
|
+
});
|
|
47
|
+
// Strip gradient from customCss when it is hoisted to the outer bg wrapper.
|
|
30
48
|
const innerCustomCss = gradientInCustomCss
|
|
31
|
-
? customCssStr.replace(/background-image\s*:[^;]+;?/gi,
|
|
49
|
+
? customCssStr.replace(/background-image\s*:[^;]+;?/gi, "").trim()
|
|
32
50
|
: customCssStr;
|
|
33
|
-
const restForStyles = gradientInCustomCss
|
|
51
|
+
const restForStyles = gradientInCustomCss
|
|
52
|
+
? { ...rest, customCss: innerCustomCss }
|
|
53
|
+
: rest;
|
|
54
|
+
// Outer td styles: strip container background when a bg-image wrapper is present
|
|
55
|
+
// so the outer wrapper's background is not double-applied.
|
|
34
56
|
const styles = (0, buildStyles_1.buildStyles)({
|
|
35
57
|
padding: textContainerPadding,
|
|
36
58
|
backgroundColor: hasBgImage ? undefined : textContainerBackgroundColor,
|
|
37
59
|
...restForStyles,
|
|
38
|
-
}, {
|
|
60
|
+
}, {
|
|
61
|
+
perChanges: [],
|
|
62
|
+
pxChanges: buildStyles_1.allPxAttributes,
|
|
63
|
+
});
|
|
39
64
|
const sanitizedText = (props.text ?? "")
|
|
40
65
|
.replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
|
|
41
66
|
.replace(/<\/p>/gi, "</div>");
|
|
42
67
|
const navigateToUrl = props.navigateToUrl || "";
|
|
43
68
|
const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
|
|
69
|
+
// Email clients apply `a { color: blue }` which overrides inherited color.
|
|
70
|
+
// Inject the block color directly onto <a> tags that don't already have one.
|
|
44
71
|
const blockTextColor = rest.color;
|
|
45
72
|
const processedText = blockTextColor
|
|
46
|
-
? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs =
|
|
73
|
+
? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = "") => {
|
|
47
74
|
if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
|
|
48
75
|
return match;
|
|
49
76
|
if (/\bstyle\s*=/i.test(attrs)) {
|
|
@@ -52,33 +79,37 @@ function convertTextBlock(blockData, cellWidthInPx) {
|
|
|
52
79
|
return `<a${attrs} style="color:${blockTextColor};">`;
|
|
53
80
|
})
|
|
54
81
|
: sanitizedText;
|
|
55
|
-
const colorStyle = blockTextColor ? `color:${blockTextColor};` :
|
|
82
|
+
const colorStyle = blockTextColor ? `color:${blockTextColor};` : "";
|
|
83
|
+
// Use display:block + width:100% so text fills the column naturally.
|
|
84
|
+
// display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
|
|
56
85
|
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
|
|
57
|
-
const safeCellWidth = cellWidthInPx
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
const safeCellWidth = cellWidthInPx
|
|
87
|
+
? Math.min(cellWidthInPx, 600)
|
|
88
|
+
: undefined;
|
|
89
|
+
// When a bg-image wrapper is present, visibilityClass moves to the outer table.
|
|
90
|
+
const textContent = (0, outlookSupport_1.appendOutlookSupport)(convertedTextBox, styles, hasBgImage ? "" : visibilityClass, safeCellWidth);
|
|
91
|
+
const linkColorStyle = blockTextColor
|
|
92
|
+
? `color:${blockTextColor};`
|
|
93
|
+
: "color:inherit;";
|
|
60
94
|
if (hasBgImage) {
|
|
61
95
|
const msoWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : 600;
|
|
62
96
|
const vmlFill = isGradient
|
|
63
97
|
? (() => {
|
|
64
98
|
const vmlAngle = (0, gradientUtils_1.cssAngleToVml)(parsedGradient?.angle || 180);
|
|
65
|
-
const c1 = parsedGradient?.fallback ||
|
|
99
|
+
const c1 = parsedGradient?.fallback || "#ffffff";
|
|
66
100
|
const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
|
|
67
101
|
return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
|
|
68
102
|
})()
|
|
69
103
|
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
70
|
-
const bgPosition = backgroundPosition ?? 'center center';
|
|
71
|
-
const bgSize = backgroundSize ?? 'cover';
|
|
72
|
-
const bgRepeat = backgroundRepeat ?? 'no-repeat';
|
|
73
104
|
const bgCss = isGradient
|
|
74
105
|
? `background:${effectiveGradient};`
|
|
75
|
-
: `background-image:url('${rawBgImageUrl}'); background-position
|
|
106
|
+
: `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`;
|
|
76
107
|
const wrappedContent = `
|
|
77
108
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
|
|
78
109
|
style="border-collapse:collapse;width:100%;max-width:${msoWidth}px;" class="${visibilityClass}">
|
|
79
110
|
<tr>
|
|
80
|
-
<td width="100%"
|
|
81
|
-
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` :
|
|
111
|
+
<td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
|
|
112
|
+
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
|
|
82
113
|
style="width:100%;max-width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
|
|
83
114
|
|
|
84
115
|
<!--[if gte mso 9]>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildStyles.d.ts","sourceRoot":"","sources":["../../src/utils/buildStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,UAA8C,CAAC;AAC7E,eAAO,MAAM,sBAAsB,UAAsB,CAAC;AAC1D,eAAO,MAAM,eAAe,UAAoD,CAAC;AAEjF,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAQ/E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"buildStyles.d.ts","sourceRoot":"","sources":["../../src/utils/buildStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,UAA8C,CAAC;AAC7E,eAAO,MAAM,sBAAsB,UAAsB,CAAC;AAC1D,eAAO,MAAM,eAAe,UAAoD,CAAC;AAEjF,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAQ/E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAkB7D;AACD,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,EACV,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,UAqFzE"}
|
|
@@ -20,12 +20,15 @@ function sanitizeFontFamily(fontFamily) {
|
|
|
20
20
|
.split(',')
|
|
21
21
|
.map(font => {
|
|
22
22
|
const trimmed = font.trim();
|
|
23
|
+
// Strip any surrounding quotes (single or double) from either end
|
|
23
24
|
const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
|
|
24
25
|
if (!unquoted)
|
|
25
26
|
return '';
|
|
27
|
+
// Generic families and single-token names need no quotes
|
|
26
28
|
if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
|
|
27
29
|
return unquoted;
|
|
28
30
|
}
|
|
31
|
+
// Multi-word font name: wrap in single quotes, escaping any embedded single quotes
|
|
29
32
|
return `'${unquoted.replace(/'/g, "\\'")}'`;
|
|
30
33
|
})
|
|
31
34
|
.filter(Boolean)
|
|
@@ -35,18 +38,28 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
35
38
|
if (!style)
|
|
36
39
|
style = {};
|
|
37
40
|
const stylesObj = {};
|
|
38
|
-
const INVALID_KEYS = [
|
|
39
|
-
"columns", "cellWidths", "cellWidth", "childWidth",
|
|
40
|
-
"visibility", "hideOnMobile", "hideOnDesktop", "label", "alignment",
|
|
41
|
-
];
|
|
42
41
|
Object.entries(style).forEach(([key, value]) => {
|
|
43
42
|
if (key === "customCss")
|
|
44
43
|
return;
|
|
44
|
+
const INVALID_KEYS = [
|
|
45
|
+
"columns",
|
|
46
|
+
"cellWidths",
|
|
47
|
+
"cellWidth",
|
|
48
|
+
"childWidth",
|
|
49
|
+
"visibility",
|
|
50
|
+
"hideOnMobile",
|
|
51
|
+
"hideOnDesktop",
|
|
52
|
+
"label",
|
|
53
|
+
"alignment",
|
|
54
|
+
];
|
|
45
55
|
if (INVALID_KEYS.includes(key))
|
|
46
56
|
return;
|
|
57
|
+
// Prevent null/undefined/"" from leaking into CSS
|
|
47
58
|
if (value === undefined || value === null || value === "")
|
|
48
59
|
return;
|
|
49
|
-
|
|
60
|
+
// FIX 1 — SANITIZE padding objects
|
|
61
|
+
if ((key === "padding" || key === "buttonPadding") &&
|
|
62
|
+
typeof value === "object") {
|
|
50
63
|
const pad = value;
|
|
51
64
|
const safePad = {
|
|
52
65
|
top: Number.isFinite(pad.top) ? pad.top : 0,
|
|
@@ -59,21 +72,26 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
59
72
|
if (key === "fontFamily" && typeof value === "string") {
|
|
60
73
|
value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
|
|
61
74
|
}
|
|
75
|
+
// Wrap backgroundImage values in url() if not already wrapped — skip gradients
|
|
62
76
|
if (key === "backgroundImage" && typeof value === "string"
|
|
63
77
|
&& !String(value).startsWith("url(")
|
|
64
78
|
&& !String(value).toLowerCase().includes("gradient(")) {
|
|
65
79
|
value = `url('${value}')`;
|
|
66
80
|
}
|
|
81
|
+
// lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
|
|
67
82
|
if (key === "lineHeight" && typeof value === "number") {
|
|
68
83
|
stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
|
|
69
84
|
return;
|
|
70
85
|
}
|
|
71
86
|
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
87
|
+
// FIX 2 — Sanitize invalid px/per values
|
|
72
88
|
if (pxChanges.includes(key)) {
|
|
73
89
|
if (typeof value === "number") {
|
|
74
|
-
|
|
90
|
+
const rounded = Math.round(value * 100) / 100;
|
|
91
|
+
stylesObj[cssKey] = `${rounded}px`;
|
|
75
92
|
}
|
|
76
93
|
else if (typeof value === "string" && value.includes("null")) {
|
|
94
|
+
// Skip invalid styles
|
|
77
95
|
return;
|
|
78
96
|
}
|
|
79
97
|
else {
|
package/dist/utils/common.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export declare function getVisibilityClass(props?: {
|
|
|
5
5
|
hideOnMobile?: boolean;
|
|
6
6
|
}): string;
|
|
7
7
|
export declare function encodeBlockPropsAttr(props: Record<string, any>): string;
|
|
8
|
+
export declare function buildOutlookBgAttr(color: string): string;
|
|
8
9
|
//# sourceMappingURL=common.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQvE"}
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQvE;AAYD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxD"}
|
package/dist/utils/common.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.extractVimeoId = exports.extractYouTubeId = void 0;
|
|
4
4
|
exports.getVisibilityClass = getVisibilityClass;
|
|
5
5
|
exports.encodeBlockPropsAttr = encodeBlockPropsAttr;
|
|
6
|
+
exports.buildOutlookBgAttr = buildOutlookBgAttr;
|
|
6
7
|
const extractYouTubeId = (url) => {
|
|
7
8
|
try {
|
|
8
9
|
const u = new URL(url);
|
|
@@ -53,3 +54,16 @@ function encodeBlockPropsAttr(props) {
|
|
|
53
54
|
.replace(/</g, "<")
|
|
54
55
|
.replace(/>/g, ">");
|
|
55
56
|
}
|
|
57
|
+
function toOutlookBgColor(color) {
|
|
58
|
+
if (!color || color === 'transparent')
|
|
59
|
+
return '';
|
|
60
|
+
const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*[\d.]+)?\s*\)/);
|
|
61
|
+
if (m) {
|
|
62
|
+
return '#' + [m[1], m[2], m[3]].map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
|
|
63
|
+
}
|
|
64
|
+
return color;
|
|
65
|
+
}
|
|
66
|
+
function buildOutlookBgAttr(color) {
|
|
67
|
+
const normalized = toOutlookBgColor(color);
|
|
68
|
+
return normalized ? ` bgcolor="${normalized}"` : '';
|
|
69
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convertJsonToHtml.d.ts","sourceRoot":"","sources":["../../src/utils/convertJsonToHtml.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,GAAU,UAAU,GAAG,
|
|
1
|
+
{"version":3,"file":"convertJsonToHtml.d.ts","sourceRoot":"","sources":["../../src/utils/convertJsonToHtml.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,GAAU,UAAU,GAAG,oBAqNpD,CAAC"}
|
|
@@ -86,9 +86,14 @@ const convertJsonToHtml = async (jsonData) => {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
@media only screen and (max-width: 600px) {
|
|
89
|
+
body {
|
|
90
|
+
overflow-x: hidden !important;
|
|
91
|
+
}
|
|
92
|
+
|
|
89
93
|
.email-container {
|
|
90
94
|
width: 100% !important;
|
|
91
95
|
max-width: 100% !important;
|
|
96
|
+
overflow-x: hidden !important;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
.stack-column,
|
|
@@ -96,6 +101,29 @@ const convertJsonToHtml = async (jsonData) => {
|
|
|
96
101
|
display: block !important;
|
|
97
102
|
width: 100% !important;
|
|
98
103
|
max-width: 100% !important;
|
|
104
|
+
/* Prevents horizontal padding from adding to 100% width once display:block is applied */
|
|
105
|
+
box-sizing: border-box !important;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Images: drop the inline pixel height so aspect ratio is preserved as width shrinks,
|
|
109
|
+
and cap width to the container. Matches the canvas ImageBlock (height:auto, max-width:100%). */
|
|
110
|
+
img {
|
|
111
|
+
height: auto !important;
|
|
112
|
+
max-width: 100% !important;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Safety net for clients that keep inline max-width:600px on nested wrappers */
|
|
116
|
+
table[style*="max-width:600px"] {
|
|
117
|
+
width: 100% !important;
|
|
118
|
+
max-width: 100% !important;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Column-gap spacer tds have no stacking behaviour — collapse them when columns stack */
|
|
122
|
+
.col-gap-spacer {
|
|
123
|
+
display: none !important;
|
|
124
|
+
width: 0 !important;
|
|
125
|
+
max-height: 0 !important;
|
|
126
|
+
overflow: hidden !important;
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
.hide-mobile {
|
|
@@ -129,6 +157,7 @@ const convertJsonToHtml = async (jsonData) => {
|
|
|
129
157
|
<table
|
|
130
158
|
role="presentation"
|
|
131
159
|
class="email-container"
|
|
160
|
+
align="center"
|
|
132
161
|
bgcolor="${canvasColor || '#ffffff'}"
|
|
133
162
|
cellpadding="0"
|
|
134
163
|
cellspacing="0"
|
|
@@ -140,6 +169,7 @@ const convertJsonToHtml = async (jsonData) => {
|
|
|
140
169
|
width: 100%;
|
|
141
170
|
max-width: 600px;
|
|
142
171
|
margin: 0 auto;
|
|
172
|
+
overflow: hidden;
|
|
143
173
|
background-color: ${canvasColor || '#ffffff'};
|
|
144
174
|
${textColor ? `color: ${textColor};` : ''}
|
|
145
175
|
${borderWidth ? `border: ${borderWidth}px ${borderStyle || 'solid'} ${borderColor || 'transparent'};` : ''}
|
|
@@ -147,7 +177,7 @@ const convertJsonToHtml = async (jsonData) => {
|
|
|
147
177
|
>
|
|
148
178
|
<tbody>
|
|
149
179
|
<tr>
|
|
150
|
-
<td style="padding: ${top}px ${right}px ${bottom}px ${left}px;">
|
|
180
|
+
<td style="padding: ${top}px ${right}px ${bottom}px ${left}px; max-width: 600px; overflow: hidden; box-sizing: border-box; word-break: break-word; overflow-wrap: anywhere;">
|
|
151
181
|
${blocksHtml.join("")}
|
|
152
182
|
</td>
|
|
153
183
|
</tr>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outlookSupport.d.ts","sourceRoot":"","sources":["../../src/utils/outlookSupport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"outlookSupport.d.ts","sourceRoot":"","sources":["../../src/utils/outlookSupport.ts"],"names":[],"mappings":"AAKA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOtD;AAOD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,UAyClB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,GAAQ,EACf,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,mBA2FrB;AAED,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAO7G"}
|
|
@@ -5,6 +5,7 @@ exports.appendOutlookSupport = appendOutlookSupport;
|
|
|
5
5
|
exports.appendOutlookForImage = appendOutlookForImage;
|
|
6
6
|
exports.loadImageNaturalDimensions = loadImageNaturalDimensions;
|
|
7
7
|
const buildStyles_1 = require("./buildStyles");
|
|
8
|
+
const common_1 = require("./common");
|
|
8
9
|
// Converts rgba(r,g,b,a) → #rrggbb for use in HTML bgcolor attributes.
|
|
9
10
|
// Old Outlook's bgcolor attribute only accepts solid hex or named colors — rgba is silently ignored.
|
|
10
11
|
function toOutlookBgColor(color) {
|
|
@@ -23,10 +24,11 @@ function extractBgColor(styleStr) {
|
|
|
23
24
|
function appendOutlookSupport(content, contentStyle, className, msoWidth) {
|
|
24
25
|
const visibilityClass = className || "";
|
|
25
26
|
const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
|
|
26
|
-
//
|
|
27
|
-
//
|
|
27
|
+
// Old Outlook (Word engine) often ignores CSS background-color on deeply-nested <td>
|
|
28
|
+
// elements. The bgcolor HTML attribute is the reliable fallback for solid colors.
|
|
29
|
+
// rgba() is stripped from CSS by the Word engine — convert to hex first.
|
|
28
30
|
const rawBg = extractBgColor(contentStyle);
|
|
29
|
-
const bgAttr =
|
|
31
|
+
const bgAttr = (0, common_1.buildOutlookBgAttr)(rawBg);
|
|
30
32
|
if (shouldHideInOutlook) {
|
|
31
33
|
return `
|
|
32
34
|
<!--[if !mso]><!-->
|
|
@@ -34,13 +36,16 @@ function appendOutlookSupport(content, contentStyle, className, msoWidth) {
|
|
|
34
36
|
<!--<![endif]-->
|
|
35
37
|
`;
|
|
36
38
|
}
|
|
39
|
+
// When an explicit pixel width is provided (e.g. inside a column cell), use dual MSO/non-MSO
|
|
40
|
+
// tables. Old Outlook (Word engine) ignores max-width and can resolve width="100%" to the
|
|
41
|
+
// full email width (600px) rather than the column width, causing images/buttons to expand.
|
|
37
42
|
if (msoWidth) {
|
|
38
43
|
return `
|
|
39
44
|
<!--[if mso]>
|
|
40
45
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" style="border-collapse:collapse;width:${msoWidth}px;"><tr><td width="${msoWidth}"${bgAttr} style="${contentStyle}">
|
|
41
46
|
<![endif]-->
|
|
42
47
|
<!--[if !mso]><!-->
|
|
43
|
-
<table data-ebr-role="wrapper" role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">
|
|
48
|
+
<table data-ebr-role="wrapper" role="presentation" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">
|
|
44
49
|
<!--<![endif]-->
|
|
45
50
|
${content}
|
|
46
51
|
<!--[if mso]></td></tr></table><![endif]-->
|
|
@@ -50,13 +55,15 @@ ${content}
|
|
|
50
55
|
`;
|
|
51
56
|
}
|
|
52
57
|
return `
|
|
53
|
-
<table data-ebr-role="wrapper" role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">${content}</td></tr></table>
|
|
58
|
+
<table data-ebr-role="wrapper" role="presentation" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" style="${buildStyles_1.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%"${bgAttr} style="${contentStyle}">${content}</td></tr></table>
|
|
54
59
|
`;
|
|
55
60
|
}
|
|
56
61
|
async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
|
|
62
|
+
// OUTLOOK FIX: Use provided dimensions or calculate from image
|
|
57
63
|
let vmlWidth;
|
|
58
64
|
let vmlHeight;
|
|
59
65
|
if (finalWidth && finalHeight) {
|
|
66
|
+
// Use pre-calculated dimensions (preferred for accuracy)
|
|
60
67
|
vmlWidth = finalWidth;
|
|
61
68
|
vmlHeight = finalHeight;
|
|
62
69
|
}
|
|
@@ -80,12 +87,18 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
80
87
|
const borderColor = style?.borderColor || "transparent";
|
|
81
88
|
const borderRadius = parseInt(style?.borderRadius) || 0;
|
|
82
89
|
const useRoundRect = borderRadius > 0;
|
|
83
|
-
const arcsize = useRoundRect
|
|
90
|
+
const arcsize = useRoundRect
|
|
91
|
+
? Math.min(borderRadius / vmlHeight, 1).toFixed(2)
|
|
92
|
+
: "";
|
|
84
93
|
const borderAttributes = borderWidth > 0
|
|
85
94
|
? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
|
|
86
95
|
: `stroked="false"`;
|
|
96
|
+
// OUTLOOK FIX: For Outlook 2019+ (version 2512), VML type="frame" causes stretching
|
|
97
|
+
// Solution: Use simple IMG tag with fixed dimensions for Outlook, only use VML for border radius
|
|
87
98
|
let outlookImage;
|
|
88
99
|
if (useRoundRect && borderRadius > 0) {
|
|
100
|
+
// Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
|
|
101
|
+
// Use aspect="atmost" to prevent image from stretching beyond its bounds
|
|
89
102
|
outlookImage = `<!--[if mso]>
|
|
90
103
|
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
91
104
|
<tr>
|
|
@@ -104,7 +117,11 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
|
|
|
104
117
|
<![endif]-->`;
|
|
105
118
|
}
|
|
106
119
|
else {
|
|
107
|
-
|
|
120
|
+
// For images without border radius, wrap in a table with explicit width for Old Outlook (Word engine)
|
|
121
|
+
// This prevents stretching/overflow in Outlook 2007-2019 and Outlook Classic
|
|
122
|
+
const borderStyleAttr = borderWidth > 0
|
|
123
|
+
? `border: ${borderWidth}px solid ${borderColor};`
|
|
124
|
+
: '';
|
|
108
125
|
outlookImage = `<!--[if mso]>
|
|
109
126
|
<table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
|
|
110
127
|
<tr>
|