email-builder-utils 1.1.44 → 1.1.46
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/jsonToHTML.d.ts +1 -1
- package/dist/utils/jsonToHTML.d.ts.map +1 -1
- package/dist/utils/jsonToHTML.js +403 -215
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ interface IBlockData {
|
|
|
23
23
|
childrenIds?: Array<string>;
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed
|
|
26
|
+
export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
|
|
27
27
|
export declare function convertToHtml(blockData: IBlockData, rootData: any, cellWidthInPx: number): Promise<string>;
|
|
28
28
|
export declare function convertVideoBlock(blockData: any, cellWidthInPx: number): Promise<string>;
|
|
29
29
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAIrC,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,
|
|
1
|
+
{"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAIrC,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AA2I/E,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AAqoCD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAwM5E"}
|
package/dist/utils/jsonToHTML.js
CHANGED
|
@@ -8,13 +8,12 @@ const common_1 = require("./common");
|
|
|
8
8
|
const fontFallback_1 = require("./fontFallback");
|
|
9
9
|
const addPxToAttributes = [
|
|
10
10
|
"fontSize",
|
|
11
|
-
"lineHeight",
|
|
12
11
|
"borderRadius",
|
|
13
12
|
"borderWidth",
|
|
14
13
|
];
|
|
15
14
|
const addPxOrPerToAttributes = ["width", "height"];
|
|
16
15
|
const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
|
|
17
|
-
exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed
|
|
16
|
+
exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
|
|
18
17
|
function encodeBlockProps(props) {
|
|
19
18
|
return JSON.stringify(props)
|
|
20
19
|
.replace(/&/g, '&')
|
|
@@ -28,6 +27,38 @@ async function loadImageNaturalDimensions(imageUrl) {
|
|
|
28
27
|
img.src = imageUrl;
|
|
29
28
|
});
|
|
30
29
|
}
|
|
30
|
+
const GENERIC_FONT_FAMILIES = new Set([
|
|
31
|
+
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
|
32
|
+
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace',
|
|
33
|
+
'ui-rounded', 'emoji', 'math', 'fangsong',
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Normalises a CSS font-family string so every multi-word family name is wrapped
|
|
37
|
+
* in single quotes — safe inside double-quoted HTML style attributes.
|
|
38
|
+
* Handles already-quoted names (single or double), generic keywords, and any
|
|
39
|
+
* number of comma-separated families.
|
|
40
|
+
*/
|
|
41
|
+
function sanitizeFontFamily(fontFamily) {
|
|
42
|
+
if (!fontFamily)
|
|
43
|
+
return fontFamily;
|
|
44
|
+
return fontFamily
|
|
45
|
+
.split(',')
|
|
46
|
+
.map(font => {
|
|
47
|
+
const trimmed = font.trim();
|
|
48
|
+
// Strip any surrounding quotes (single or double) from either end
|
|
49
|
+
const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
|
|
50
|
+
if (!unquoted)
|
|
51
|
+
return '';
|
|
52
|
+
// Generic families and single-token names need no quotes
|
|
53
|
+
if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
|
|
54
|
+
return unquoted;
|
|
55
|
+
}
|
|
56
|
+
// Multi-word font name: wrap in single quotes, escaping any embedded single quotes
|
|
57
|
+
return `'${unquoted.replace(/'/g, "\\'")}'`;
|
|
58
|
+
})
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(', ');
|
|
61
|
+
}
|
|
31
62
|
function buildStyles(style, { pxChanges, perChanges }) {
|
|
32
63
|
if (!style)
|
|
33
64
|
style = {};
|
|
@@ -64,7 +95,7 @@ function buildStyles(style, { pxChanges, perChanges }) {
|
|
|
64
95
|
value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
|
|
65
96
|
}
|
|
66
97
|
if (key === "fontFamily" && typeof value === "string") {
|
|
67
|
-
value = (0, fontFallback_1.withFontFallback)(value)
|
|
98
|
+
value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
|
|
68
99
|
}
|
|
69
100
|
// Wrap backgroundImage values in url() if not already wrapped — skip gradients
|
|
70
101
|
if (key === "backgroundImage" && typeof value === "string"
|
|
@@ -170,12 +201,18 @@ ${content}
|
|
|
170
201
|
function convertDividerBlockToHtml(blockData) {
|
|
171
202
|
const { style, props } = blockData.data;
|
|
172
203
|
const { hideOnMobile, hideOnDesktop } = props;
|
|
173
|
-
const { thickness, dividerColor, width, ...rest } = style;
|
|
204
|
+
const { thickness, dividerColor, width, alignment, ...rest } = style;
|
|
174
205
|
const convertedStyle = buildStyles(rest, {
|
|
175
206
|
perChanges: [],
|
|
176
207
|
pxChanges: allPxAttributes,
|
|
177
208
|
});
|
|
178
|
-
const dividerWidth = width ||
|
|
209
|
+
const dividerWidth = width || 100;
|
|
210
|
+
const alignAttr = alignment === 'center' ? 'center' : alignment === 'right' ? 'right' : 'left';
|
|
211
|
+
const alignMargin = alignAttr === 'center' ? '0 auto' : alignAttr === 'right' ? '0 0 0 auto' : '0 auto 0 0';
|
|
212
|
+
// Append text-align so the import parser can recover alignment via inheritance
|
|
213
|
+
const contentStyle = convertedStyle
|
|
214
|
+
? `${convertedStyle}; text-align:${alignAttr};`
|
|
215
|
+
: `text-align:${alignAttr};`;
|
|
179
216
|
// Build class name based on visibility
|
|
180
217
|
const visibilityClass = [
|
|
181
218
|
hideOnMobile ? "hide-mobile" : "",
|
|
@@ -185,21 +222,23 @@ function convertDividerBlockToHtml(blockData) {
|
|
|
185
222
|
.join(" ");
|
|
186
223
|
const dividerContent = `
|
|
187
224
|
<table
|
|
225
|
+
align="${alignAttr}"
|
|
188
226
|
width="${dividerWidth}%"
|
|
189
227
|
cellpadding="0"
|
|
190
228
|
cellspacing="0"
|
|
229
|
+
style="margin:${alignMargin};"
|
|
191
230
|
>
|
|
192
231
|
<tr>
|
|
193
232
|
<td
|
|
194
233
|
height="${thickness}"
|
|
195
|
-
style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth}
|
|
234
|
+
style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth}%;"
|
|
196
235
|
>
|
|
197
236
|
|
|
198
237
|
</td>
|
|
199
238
|
</tr>
|
|
200
239
|
</table>
|
|
201
240
|
`;
|
|
202
|
-
return appendOutlookSupport(dividerContent,
|
|
241
|
+
return appendOutlookSupport(dividerContent, contentStyle, visibilityClass);
|
|
203
242
|
}
|
|
204
243
|
function convertSpacerBlockToHtml(blockData) {
|
|
205
244
|
const { style, props } = blockData.data;
|
|
@@ -213,8 +252,27 @@ function convertSpacerBlockToHtml(blockData) {
|
|
|
213
252
|
function convertTextBlock(blockData, cellWidthInPx) {
|
|
214
253
|
const { style, props } = blockData.data;
|
|
215
254
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
216
|
-
const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
|
|
255
|
+
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
|
|
217
256
|
...rest } = style;
|
|
257
|
+
// Detect background image or gradient (may live in backgroundImage or customCss).
|
|
258
|
+
// Same multi-client approach as Grid block: outer wrapper <td> carries the background
|
|
259
|
+
// via CSS (Gmail/New Outlook), background attribute (Yahoo), and VML (Old Outlook).
|
|
260
|
+
const bgImageStr = typeof backgroundImage === 'string' ? backgroundImage : '';
|
|
261
|
+
const customCssStr = rest.customCss || '';
|
|
262
|
+
const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
|
|
263
|
+
? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
|
|
264
|
+
: '';
|
|
265
|
+
const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
|
|
266
|
+
const isGradient = Boolean(effectiveGradient);
|
|
267
|
+
const parsedGradient = isGradient ? parseGradient(effectiveGradient) : null;
|
|
268
|
+
const rawBgImageUrl = !isGradient && bgImageStr
|
|
269
|
+
? bgImageStr.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '')
|
|
270
|
+
: null;
|
|
271
|
+
const hasBgImage = Boolean(rawBgImageUrl || isGradient);
|
|
272
|
+
const fallbackBgColor = textContainerBackgroundColor ||
|
|
273
|
+
parsedGradient?.fallback ||
|
|
274
|
+
extractCssFallbackColor(customCssStr) ||
|
|
275
|
+
'#ffffff';
|
|
218
276
|
// Text box decoration styles (border, background, padding) — no width
|
|
219
277
|
const textBoxStyle = {
|
|
220
278
|
backgroundColor,
|
|
@@ -228,11 +286,17 @@ function convertTextBlock(blockData, cellWidthInPx) {
|
|
|
228
286
|
perChanges: [],
|
|
229
287
|
pxChanges: allPxAttributes,
|
|
230
288
|
});
|
|
231
|
-
//
|
|
289
|
+
// Strip gradient from customCss when it is hoisted to the outer bg wrapper.
|
|
290
|
+
const innerCustomCss = gradientInCustomCss
|
|
291
|
+
? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
|
|
292
|
+
: customCssStr;
|
|
293
|
+
const restForStyles = gradientInCustomCss ? { ...rest, customCss: innerCustomCss } : rest;
|
|
294
|
+
// Outer td styles: strip container background when a bg-image wrapper is present
|
|
295
|
+
// so the outer wrapper's background is not double-applied.
|
|
232
296
|
const styles = buildStyles({
|
|
233
297
|
padding: textContainerPadding,
|
|
234
|
-
backgroundColor: textContainerBackgroundColor,
|
|
235
|
-
...
|
|
298
|
+
backgroundColor: hasBgImage ? undefined : textContainerBackgroundColor,
|
|
299
|
+
...restForStyles,
|
|
236
300
|
}, {
|
|
237
301
|
perChanges: [],
|
|
238
302
|
pxChanges: allPxAttributes,
|
|
@@ -242,13 +306,72 @@ function convertTextBlock(blockData, cellWidthInPx) {
|
|
|
242
306
|
.replace(/<\/p>/gi, "</div>");
|
|
243
307
|
const navigateToUrl = props.navigateToUrl || "";
|
|
244
308
|
const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
|
|
309
|
+
// Email clients apply `a { color: blue }` which overrides inherited color.
|
|
310
|
+
// Inject the block color directly onto <a> tags that don't already have one.
|
|
311
|
+
const blockTextColor = rest.color;
|
|
312
|
+
const processedText = blockTextColor
|
|
313
|
+
? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = '') => {
|
|
314
|
+
if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
|
|
315
|
+
return match;
|
|
316
|
+
if (/\bstyle\s*=/i.test(attrs)) {
|
|
317
|
+
return `<a${attrs.replace(/(\bstyle\s*=\s*["'])/, `$1color:${blockTextColor};`)}>`;
|
|
318
|
+
}
|
|
319
|
+
return `<a${attrs} style="color:${blockTextColor};">`;
|
|
320
|
+
})
|
|
321
|
+
: sanitizedText;
|
|
322
|
+
const colorStyle = blockTextColor ? `color:${blockTextColor};` : '';
|
|
245
323
|
// Use display:block + width:100% so text fills the column naturally.
|
|
246
324
|
// display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
|
|
247
|
-
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${fontSizeStyle}${convertedTextStyle}">${
|
|
325
|
+
const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
|
|
248
326
|
const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
|
|
249
|
-
|
|
327
|
+
// When a bg-image wrapper is present, visibilityClass moves to the outer table.
|
|
328
|
+
const textContent = appendOutlookSupport(convertedTextBox, styles, hasBgImage ? '' : visibilityClass, safeCellWidth);
|
|
329
|
+
const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
|
|
330
|
+
if (hasBgImage) {
|
|
331
|
+
const msoWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : 600;
|
|
332
|
+
const vmlFill = isGradient
|
|
333
|
+
? (() => {
|
|
334
|
+
const vmlAngle = cssAngleToVml(parsedGradient?.angle || 180);
|
|
335
|
+
const c1 = parsedGradient?.fallback || '#ffffff';
|
|
336
|
+
const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
|
|
337
|
+
return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
|
|
338
|
+
})()
|
|
339
|
+
: `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
|
|
340
|
+
const bgCss = isGradient
|
|
341
|
+
? `background:${effectiveGradient};`
|
|
342
|
+
: `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`;
|
|
343
|
+
const wrappedContent = `
|
|
344
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" role="presentation"
|
|
345
|
+
style="border-collapse:collapse;width:${msoWidth}px;" class="${visibilityClass}">
|
|
346
|
+
<tr>
|
|
347
|
+
<td width="${msoWidth}" bgcolor="${fallbackBgColor}" valign="top"
|
|
348
|
+
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ''}
|
|
349
|
+
style="width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
|
|
350
|
+
|
|
351
|
+
<!--[if gte mso 9]>
|
|
352
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml"
|
|
353
|
+
fill="true" stroke="false"
|
|
354
|
+
style="width:${msoWidth}px;">
|
|
355
|
+
${vmlFill}
|
|
356
|
+
<v:textbox inset="0,0,0,0">
|
|
357
|
+
<![endif]-->
|
|
358
|
+
|
|
359
|
+
${textContent}
|
|
360
|
+
|
|
361
|
+
<!--[if gte mso 9]>
|
|
362
|
+
</v:textbox>
|
|
363
|
+
</v:rect>
|
|
364
|
+
<![endif]-->
|
|
365
|
+
|
|
366
|
+
</td>
|
|
367
|
+
</tr>
|
|
368
|
+
</table>`;
|
|
369
|
+
return navigateToUrl
|
|
370
|
+
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${wrappedContent}</a>`
|
|
371
|
+
: wrappedContent;
|
|
372
|
+
}
|
|
250
373
|
return navigateToUrl
|
|
251
|
-
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="
|
|
374
|
+
? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${textContent}</a>`
|
|
252
375
|
: textContent;
|
|
253
376
|
}
|
|
254
377
|
async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
|
|
@@ -416,161 +539,138 @@ async function convertImageBlock(blockData, cellWidthInPx) {
|
|
|
416
539
|
? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
|
|
417
540
|
: imageContent;
|
|
418
541
|
}
|
|
419
|
-
function appendOutlookForButton(
|
|
420
|
-
const
|
|
542
|
+
function appendOutlookForButton(buttonData) {
|
|
543
|
+
const { style, text, navigateToUrl } = buttonData;
|
|
544
|
+
const pad = style.buttonPadding || {};
|
|
421
545
|
const padTop = Number.isFinite(pad.top) ? pad.top : 10;
|
|
422
546
|
const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
|
|
423
547
|
const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
|
|
424
548
|
const padRight = Number.isFinite(pad.right) ? pad.right : 20;
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
<
|
|
549
|
+
const fs = style.fontSize || 16;
|
|
550
|
+
const minHeight = padTop + padBottom + fs;
|
|
551
|
+
const finalHeight = typeof style.height === "number" && style.height > 0
|
|
552
|
+
? Math.max(style.height, minHeight)
|
|
553
|
+
: minHeight;
|
|
554
|
+
const safeFF = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(style.fontFamily));
|
|
555
|
+
const safeColor = style.color || "#ffffff";
|
|
556
|
+
const bgColor = style.buttonColor || "transparent";
|
|
557
|
+
const bdColor = style.borderColor || "transparent";
|
|
558
|
+
const bdStyle = style.borderStyle || "solid";
|
|
559
|
+
const bw = typeof style.borderWidth === "number" ? style.borderWidth : 0;
|
|
560
|
+
const br = typeof style.borderRadius === "number" ? style.borderRadius : 0;
|
|
561
|
+
const fontWeight = style.fontWeight || 400;
|
|
562
|
+
const containerAlign = style.alignment || style.textAlign || "left";
|
|
563
|
+
const explicitWidth = typeof style.width === "number" && style.width > 0 ? style.width : 0;
|
|
564
|
+
const borderCss = bw > 0 ? `border:${bw}px ${bdStyle} ${bdColor};` : "";
|
|
565
|
+
const widthCss = explicitWidth ? `width:${explicitWidth}px;` : "";
|
|
566
|
+
// ── Non-MSO: <a display:inline-block> — border-radius works in modern clients ──
|
|
567
|
+
const nonMsoAnchor = `<!--[if !mso]><!-->
|
|
568
|
+
<a href="${navigateToUrl}"
|
|
569
|
+
target="_blank" rel="noreferrer noopener"
|
|
570
|
+
style="display:inline-block;background-color:${bgColor};border-radius:${br}px;${borderCss}color:${safeColor};font-family:${safeFF};font-size:${fs}px;font-weight:${fontWeight};text-decoration:none;padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;line-height:${fs}px;text-align:center;white-space:nowrap;-webkit-text-size-adjust:none;box-sizing:border-box;${widthCss}mso-hide:all;">${text}</a>
|
|
571
|
+
<!--<![endif]-->`;
|
|
572
|
+
// ── MSO: table-based bulletproof button.
|
|
573
|
+
// <v:roundrect arcsize="50%"> renders its half-circle arcs as visible bracket shapes
|
|
574
|
+
// in Outlook, so we use a plain <table>+<td> instead. bgcolor on <td> is reliable in
|
|
575
|
+
// all classic Outlook versions; border-radius is not (square corners in Outlook).
|
|
576
|
+
const tdStyleParts = [`padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px`];
|
|
577
|
+
if (bw > 0)
|
|
578
|
+
tdStyleParts.push(`border:${bw}px ${bdStyle} ${bdColor}`);
|
|
579
|
+
if (explicitWidth)
|
|
580
|
+
tdStyleParts.push(`width:${explicitWidth}px`);
|
|
581
|
+
const tdStyle = tdStyleParts.join(";");
|
|
582
|
+
const msoButton = `<!--[if mso]>
|
|
583
|
+
<table cellspacing="0" cellpadding="0" border="0">
|
|
445
584
|
<tr>
|
|
446
|
-
<td
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
${finalHeight ? `height="${finalHeight}"` : ""}
|
|
450
|
-
bgcolor="${bgColor}"
|
|
451
|
-
style="
|
|
452
|
-
${finalHeight ? `height:${finalHeight}px;` : ""}
|
|
453
|
-
background-color:${bgColor};
|
|
454
|
-
border-radius:${borderRadius}px;
|
|
455
|
-
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
456
|
-
overflow:hidden;
|
|
457
|
-
mso-line-height-rule:exactly;
|
|
458
|
-
">
|
|
459
|
-
|
|
460
|
-
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
461
|
-
<tr>
|
|
462
|
-
<td align="center" valign="middle"
|
|
463
|
-
style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
|
|
464
|
-
|
|
465
|
-
<a href="${navigateToUrl}"
|
|
466
|
-
style="
|
|
467
|
-
display:inline-block;
|
|
468
|
-
color:${color};
|
|
469
|
-
text-decoration:none;
|
|
470
|
-
font-family:${fontFamily};
|
|
471
|
-
font-size:${fontSize}px;
|
|
472
|
-
font-weight:${fontWeight};
|
|
473
|
-
line-height:normal;
|
|
474
|
-
">
|
|
475
|
-
${text}
|
|
476
|
-
</a>
|
|
477
|
-
|
|
478
|
-
</td>
|
|
479
|
-
</tr>
|
|
480
|
-
</table>
|
|
481
|
-
|
|
585
|
+
<td bgcolor="${bgColor}" style="${tdStyle};">
|
|
586
|
+
<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener"
|
|
587
|
+
style="color:${safeColor};font-family:${safeFF};font-size:${fs}px;font-weight:${fontWeight};text-decoration:none;display:inline-block;line-height:${fs}px;">${text}</a>
|
|
482
588
|
</td>
|
|
483
589
|
</tr>
|
|
484
590
|
</table>
|
|
485
|
-
<![endif]
|
|
486
|
-
|
|
487
|
-
${
|
|
488
|
-
|
|
591
|
+
<![endif]-->`;
|
|
592
|
+
const innerContent = containerAlign === "center"
|
|
593
|
+
? `<center>${msoButton}${nonMsoAnchor}</center>`
|
|
594
|
+
: `<div style="text-align:${containerAlign};">${msoButton}${nonMsoAnchor}</div>`;
|
|
595
|
+
return {
|
|
596
|
+
innerContent,
|
|
597
|
+
computed: {
|
|
598
|
+
fs,
|
|
599
|
+
fontWeight,
|
|
600
|
+
containerAlign,
|
|
601
|
+
padTop,
|
|
602
|
+
padRight,
|
|
603
|
+
padBottom,
|
|
604
|
+
padLeft,
|
|
605
|
+
explicitWidth,
|
|
606
|
+
safeColor,
|
|
607
|
+
bgColor,
|
|
608
|
+
safeFF,
|
|
609
|
+
finalHeight,
|
|
610
|
+
bw,
|
|
611
|
+
br,
|
|
612
|
+
bdColor,
|
|
613
|
+
bdStyle,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
489
616
|
}
|
|
490
617
|
function convertButtonBlock(blockData) {
|
|
491
618
|
const { style, props } = blockData.data;
|
|
492
619
|
const { text, navigateToUrl } = props;
|
|
493
|
-
const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg,
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
<a href="${navigateToUrl}"
|
|
542
|
-
style="
|
|
543
|
-
display:inline-block;
|
|
544
|
-
color:${safeColor};
|
|
545
|
-
text-decoration:none;
|
|
546
|
-
font-family:${safeFF};
|
|
547
|
-
font-size:${fs}px;
|
|
548
|
-
font-weight:${fontWeight || 400};
|
|
549
|
-
line-height:normal;
|
|
550
|
-
white-space:nowrap;
|
|
551
|
-
">
|
|
552
|
-
${text}
|
|
553
|
-
</a>
|
|
554
|
-
|
|
555
|
-
</td>
|
|
556
|
-
</tr>
|
|
557
|
-
</table>
|
|
558
|
-
|
|
559
|
-
</td>
|
|
560
|
-
</tr>
|
|
561
|
-
</table>
|
|
562
|
-
`;
|
|
563
|
-
const aligned = containerAlign === "center"
|
|
564
|
-
? `<center>${buttonTable}</center>`
|
|
565
|
-
: `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
|
|
566
|
-
const buttonWithOutlook = appendOutlookForButton(aligned, style, navigateToUrl, text);
|
|
620
|
+
const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, } = style;
|
|
621
|
+
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
622
|
+
const { innerContent, computed } = appendOutlookForButton({
|
|
623
|
+
style: {
|
|
624
|
+
fontFamily,
|
|
625
|
+
fontSize,
|
|
626
|
+
fontWeight,
|
|
627
|
+
textAlign,
|
|
628
|
+
borderColor,
|
|
629
|
+
borderRadius,
|
|
630
|
+
borderWidth,
|
|
631
|
+
borderStyle,
|
|
632
|
+
buttonPadding,
|
|
633
|
+
color,
|
|
634
|
+
buttonColor,
|
|
635
|
+
width,
|
|
636
|
+
height,
|
|
637
|
+
alignment,
|
|
638
|
+
},
|
|
639
|
+
text: text || "",
|
|
640
|
+
navigateToUrl: navigateToUrl || "",
|
|
641
|
+
});
|
|
642
|
+
const buttonBlockProps = encodeBlockProps({
|
|
643
|
+
buttonText: text || '',
|
|
644
|
+
navigateToUrl: navigateToUrl || '',
|
|
645
|
+
buttonColor: computed.bgColor,
|
|
646
|
+
color: computed.safeColor,
|
|
647
|
+
fontFamily: fontFamily || '',
|
|
648
|
+
fontSize: computed.fs,
|
|
649
|
+
fontWeight: computed.fontWeight,
|
|
650
|
+
alignment: computed.containerAlign,
|
|
651
|
+
padding: {
|
|
652
|
+
top: padding?.top || 0,
|
|
653
|
+
right: padding?.right || 0,
|
|
654
|
+
bottom: padding?.bottom || 0,
|
|
655
|
+
left: padding?.left || 0,
|
|
656
|
+
},
|
|
657
|
+
buttonPadding: { top: computed.padTop, right: computed.padRight, bottom: computed.padBottom, left: computed.padLeft },
|
|
658
|
+
width: computed.explicitWidth,
|
|
659
|
+
height: typeof height === 'number' && height > 0 ? height : 0,
|
|
660
|
+
backgroundColor: containerBg || '',
|
|
661
|
+
borderRadius: computed.br,
|
|
662
|
+
borderColor: computed.bdColor,
|
|
663
|
+
borderWidth: computed.bw,
|
|
664
|
+
borderStyle: computed.bdStyle,
|
|
665
|
+
hideOnDesktop: Boolean(props.hideOnDesktop),
|
|
666
|
+
hideOnMobile: Boolean(props.hideOnMobile),
|
|
667
|
+
});
|
|
567
668
|
return `
|
|
568
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
|
669
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" class="${visibilityClass}" data-block-type="button" data-block-props="${buttonBlockProps}">
|
|
569
670
|
<tr>
|
|
570
|
-
<td align="${containerAlign}"
|
|
571
|
-
style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
|
|
572
|
-
|
|
573
|
-
${buttonWithOutlook}
|
|
671
|
+
<td align="${computed.containerAlign}"
|
|
672
|
+
style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;background-color:${containerBg || 'transparent'};">
|
|
673
|
+
${innerContent}
|
|
574
674
|
</td>
|
|
575
675
|
</tr>
|
|
576
676
|
</table>
|
|
@@ -684,10 +784,29 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
684
784
|
: customCssStr;
|
|
685
785
|
// Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
|
|
686
786
|
// background props from the inner table so the outer <td> background shows through.
|
|
687
|
-
const
|
|
787
|
+
const innerRestStyleRaw = (rawBgImageUrl || isGradient)
|
|
688
788
|
? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
|
|
689
789
|
: { ...restStyle, customCss: innerCustomCss };
|
|
690
|
-
|
|
790
|
+
// Extract border/radius props — applied via a div wrapper for non-MSO clients so that
|
|
791
|
+
// border-radius is honoured (Gmail/Outlook compose strip border-radius from <table>).
|
|
792
|
+
const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
|
|
793
|
+
const divBorderParts = [];
|
|
794
|
+
if (borderRadius)
|
|
795
|
+
divBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
|
|
796
|
+
if (border) {
|
|
797
|
+
divBorderParts.push(`border:${border};`);
|
|
798
|
+
}
|
|
799
|
+
else if (borderWidth || borderColor || borderStyleProp) {
|
|
800
|
+
const bw = borderWidth ? (typeof borderWidth === 'number' ? borderWidth + 'px' : borderWidth) : '1px';
|
|
801
|
+
const bs = borderStyleProp || 'solid';
|
|
802
|
+
const bc = borderColor || '#000000';
|
|
803
|
+
divBorderParts.push(`border:${bw} ${bs} ${bc};`);
|
|
804
|
+
}
|
|
805
|
+
const divBorderStyle = divBorderParts.join(' ');
|
|
806
|
+
const tableBgForNonMso = divBorderStyle
|
|
807
|
+
? 'transparent'
|
|
808
|
+
: ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
|
|
809
|
+
const tableStyles = buildStyles({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
|
|
691
810
|
perChanges: [],
|
|
692
811
|
pxChanges: allPxAttributes,
|
|
693
812
|
});
|
|
@@ -707,7 +826,15 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
707
826
|
const innerBgTransparent = (rawBgImageUrl || isGradient)
|
|
708
827
|
? 'background-color:transparent;'
|
|
709
828
|
: '';
|
|
710
|
-
const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor ? ` bgcolor="${backgroundColor}"` : '';
|
|
829
|
+
const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor && !divBorderStyle ? ` bgcolor="${backgroundColor}"` : '';
|
|
830
|
+
// When divBorderStyle is set the non-MSO <table> is transparent, so the Grid's
|
|
831
|
+
// backgroundColor must move onto the div wrapper — otherwise it vanishes in modern clients.
|
|
832
|
+
// Skip this for bg-image/gradient blocks; they apply their background via a separate wrapper.
|
|
833
|
+
const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
|
|
834
|
+
? ` background-color:${backgroundColor};`
|
|
835
|
+
: '';
|
|
836
|
+
const divWrapOpen = divBorderStyle ? `<div style="${divBorderStyle}${divWrapBg}">` : '';
|
|
837
|
+
const divWrapClose = divBorderStyle ? `</div>` : '';
|
|
711
838
|
let html = `
|
|
712
839
|
<!--[if mso]>
|
|
713
840
|
<table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
|
|
@@ -773,7 +900,7 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
773
900
|
const childVisible = !childProps.hideOnDesktop;
|
|
774
901
|
const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
|
|
775
902
|
if (childVisible) {
|
|
776
|
-
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth);
|
|
903
|
+
const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
|
|
777
904
|
// bgcolor on the cell <td> ensures background-color survives Outlook
|
|
778
905
|
// compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
|
|
779
906
|
const cellBgColor = cellStyle.backgroundColor || '';
|
|
@@ -843,10 +970,11 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
843
970
|
style="border-collapse:collapse;width:${msoTableWidth}px;">
|
|
844
971
|
<tr>
|
|
845
972
|
<td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
|
|
973
|
+
${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
|
|
846
974
|
style="
|
|
847
975
|
width:${msoTableWidth}px;
|
|
848
976
|
background-color:${fallbackBgColor};
|
|
849
|
-
${isGradient ? `background:${effectiveGradient};` : `background:url('${rawBgImageUrl}') center
|
|
977
|
+
${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
|
|
850
978
|
">
|
|
851
979
|
|
|
852
980
|
<!--[if gte mso 9]>
|
|
@@ -868,12 +996,32 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
|
|
|
868
996
|
</tr>
|
|
869
997
|
</table>`;
|
|
870
998
|
}
|
|
999
|
+
// Wrap the entire grid (including any bg-image outer table) in a div when the block
|
|
1000
|
+
// has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
|
|
1001
|
+
// — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
|
|
1002
|
+
// border-radius on <div> but still shows the rectangular border; new Outlook works fully.
|
|
1003
|
+
if (divBorderStyle)
|
|
1004
|
+
html = `${divWrapOpen}${html}${divWrapClose}`;
|
|
871
1005
|
return html;
|
|
872
1006
|
}
|
|
873
|
-
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
|
|
1007
|
+
async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
|
|
874
1008
|
const { style = {}, childrenIds = [], props = {} } = blockData.data;
|
|
875
1009
|
const visibilityClass = (0, common_1.getVisibilityClass)(props);
|
|
876
|
-
|
|
1010
|
+
// Extract border + radius from style so they move to the div wrapper (not the <td>).
|
|
1011
|
+
// Gmail strips border-radius from <td> but honours it on <div>. By putting border and
|
|
1012
|
+
// radius on the same unconditional <div>, the rounded card border renders in all clients.
|
|
1013
|
+
// The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
|
|
1014
|
+
const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
|
|
1015
|
+
// backgroundColor must stay on the div wrapper (not the <td>) in two cases:
|
|
1016
|
+
// 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
|
|
1017
|
+
// 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
|
|
1018
|
+
// In both cases, the rectangular <td> background bleeds through rounded corners if kept
|
|
1019
|
+
// in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
|
|
1020
|
+
const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
|
|
1021
|
+
const styleForTd = stripBgFromTd
|
|
1022
|
+
? { ...styleWithoutBorder, backgroundColor: 'transparent' }
|
|
1023
|
+
: styleWithoutBorder;
|
|
1024
|
+
const styles = buildStyles(styleForTd, {
|
|
877
1025
|
perChanges: [],
|
|
878
1026
|
pxChanges: allPxAttributes,
|
|
879
1027
|
});
|
|
@@ -884,7 +1032,7 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
884
1032
|
// Subtract the cell's own padding so children receive the actual content-area width.
|
|
885
1033
|
// Old Outlook honours explicit img/table width attributes — if a child is sized to the
|
|
886
1034
|
// full column width (ignoring padding) it overflows and expands the column.
|
|
887
|
-
const cellPad =
|
|
1035
|
+
const cellPad = styleWithoutBorder?.padding || {};
|
|
888
1036
|
const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
|
|
889
1037
|
const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
|
|
890
1038
|
const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
|
|
@@ -896,22 +1044,35 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
|
|
|
896
1044
|
parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
|
|
897
1045
|
}
|
|
898
1046
|
}
|
|
899
|
-
const borderRadius =
|
|
900
|
-
const bgColor =
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1047
|
+
const borderRadius = cellBorderRadius || 0;
|
|
1048
|
+
const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
|
|
1049
|
+
// Build border CSS for the div wrapper.
|
|
1050
|
+
// When the parent grid already has a divBorderStyle wrapper (border + border-radius +
|
|
1051
|
+
// overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
|
|
1052
|
+
// two concentric borders of the same colour (double-border). The grid's wrapper div
|
|
1053
|
+
// already provides the visual container; the cell div only needs background-color.
|
|
1054
|
+
const cellDivBorderParts = [];
|
|
1055
|
+
if (!parentGridHasBorder) {
|
|
1056
|
+
if (borderRadius)
|
|
1057
|
+
cellDivBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
|
|
1058
|
+
if (cellBorderShorthand) {
|
|
1059
|
+
cellDivBorderParts.push(`border:${cellBorderShorthand};`);
|
|
1060
|
+
}
|
|
1061
|
+
else if (cellBorderWidth || cellBorderColor || cellBorderStyleProp) {
|
|
1062
|
+
const bw = cellBorderWidth ? (typeof cellBorderWidth === 'number' ? cellBorderWidth + 'px' : cellBorderWidth) : '1px';
|
|
1063
|
+
const bs = cellBorderStyleProp || 'solid';
|
|
1064
|
+
const bc = cellBorderColor || '#000000';
|
|
1065
|
+
cellDivBorderParts.push(`border:${bw} ${bs} ${bc};`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const cellDivBorderStyle = cellDivBorderParts.join(' ');
|
|
1069
|
+
// Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
|
|
1070
|
+
// background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
|
|
1071
|
+
const divStyleParts = [`background-color:${bgColor};`];
|
|
1072
|
+
if (cellDivBorderStyle)
|
|
1073
|
+
divStyleParts.push(cellDivBorderStyle);
|
|
1074
|
+
const divStyleStr = divStyleParts.join(' ');
|
|
1075
|
+
const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
|
|
915
1076
|
return {
|
|
916
1077
|
html: wrapped,
|
|
917
1078
|
styles,
|
|
@@ -1028,16 +1189,28 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
|
1028
1189
|
<![endif]-->`;
|
|
1029
1190
|
// Non-Outlook: use a real <img> for the thumbnail so it renders in Gmail / Yahoo / webmail.
|
|
1030
1191
|
// background-image on <table> is stripped by virtually every email client.
|
|
1031
|
-
// position:absolute for the play-button overlay is safe here because this block
|
|
1032
|
-
// is already inside <!--[if !mso]> — Outlook is handled separately via VML above.
|
|
1033
1192
|
const thumbnailW = Math.round(innerContainerWidth);
|
|
1034
1193
|
const thumbnailH = Math.round(calculatedHeight);
|
|
1035
|
-
const playMarginTop = -Math.round(playIconHeight / 2);
|
|
1036
|
-
const playMarginLeft = -Math.round(playIconWidth / 2);
|
|
1037
1194
|
const borderAttr = borderWidth > 0 ? `border:${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};` : "border:0;";
|
|
1038
1195
|
const radiusAttr = borderRadius > 0 ? `border-radius:${borderRadius}px; overflow:hidden;` : "";
|
|
1196
|
+
// Overlay the play button using negative margin-top + line-height centering.
|
|
1197
|
+
// This avoids both position:absolute (stripped by Gmail/Yahoo) and
|
|
1198
|
+
// height:0/overflow:visible (clipped by New Outlook at <td> boundaries).
|
|
1199
|
+
// The play button <a> is pulled up by margin-top:-thumbnailH to sit over the thumbnail,
|
|
1200
|
+
// then line-height:thumbnailH + vertical-align:middle centres the icon vertically.
|
|
1201
|
+
// Elements later in DOM flow render on top of earlier ones, so the play icon overlays the image.
|
|
1202
|
+
const playButtonHtml = `<a href="${videoLink}" target="_blank" data-play-button="true"
|
|
1203
|
+
style="display:block; margin-top:-${thumbnailH}px; text-align:center; line-height:${thumbnailH}px; font-size:0; text-decoration:none; border:0; outline:none;">
|
|
1204
|
+
<img
|
|
1205
|
+
src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
|
|
1206
|
+
width="${playIconWidth}"
|
|
1207
|
+
height="${playIconHeight}"
|
|
1208
|
+
alt="Play"
|
|
1209
|
+
style="display:inline-block; vertical-align:middle; border:0; outline:none;"
|
|
1210
|
+
/>
|
|
1211
|
+
</a>`;
|
|
1039
1212
|
const nonOutlookVideoContent = `<!--[if !mso]><!-->
|
|
1040
|
-
<div style="display:block; width:100%; max-width:${thumbnailW}px;
|
|
1213
|
+
<div style="display:block; width:100%; max-width:${thumbnailW}px; line-height:0; font-size:0; ${borderAttr}${radiusAttr}">
|
|
1041
1214
|
<a href="${videoLink}" target="_blank" style="display:block; text-decoration:none; line-height:0; font-size:0;">
|
|
1042
1215
|
<img
|
|
1043
1216
|
src="${resolvedThumbnail}"
|
|
@@ -1046,21 +1219,32 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
|
|
|
1046
1219
|
alt="${altText || "Video"}"
|
|
1047
1220
|
style="display:block; width:100%; max-width:${thumbnailW}px; height:auto; border:0;"
|
|
1048
1221
|
/>
|
|
1049
|
-
<img
|
|
1050
|
-
src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
|
|
1051
|
-
width="${playIconWidth}"
|
|
1052
|
-
alt="Play"
|
|
1053
|
-
style="display:block; position:absolute; top:50%; left:50%; margin-top:${playMarginTop}px; margin-left:${playMarginLeft}px; border:0; outline:none;"
|
|
1054
|
-
/>
|
|
1055
1222
|
</a>
|
|
1223
|
+
${playButtonHtml}
|
|
1056
1224
|
</div>
|
|
1057
1225
|
<!--<![endif]-->`;
|
|
1058
1226
|
const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
|
|
1227
|
+
const videoBlockProps = encodeBlockProps({
|
|
1228
|
+
videoUrl: videoUrl || '',
|
|
1229
|
+
youtubeVideoUrl: youtubeVideoUrl || '',
|
|
1230
|
+
thumbnailUrl: thumbnailUrl || resolvedThumbnail,
|
|
1231
|
+
altText: altText || '',
|
|
1232
|
+
width: parseFloat(percentWidth) || 100,
|
|
1233
|
+
padding: style?.padding || { top: 0, right: 0, bottom: 0, left: 0 },
|
|
1234
|
+
backgroundColor: style?.backgroundColor || '',
|
|
1235
|
+
textAlign: style?.textAlign || 'left',
|
|
1236
|
+
borderRadius: style?.borderRadius || 0,
|
|
1237
|
+
borderColor: style?.borderColor || '',
|
|
1238
|
+
borderWidth: style?.borderWidth || 0,
|
|
1239
|
+
borderStyle: style?.borderStyle || 'none',
|
|
1240
|
+
hideOnDesktop: Boolean(props.hideOnDesktop),
|
|
1241
|
+
hideOnMobile: Boolean(props.hideOnMobile),
|
|
1242
|
+
});
|
|
1059
1243
|
const wrapperHtml = `
|
|
1060
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
|
|
1244
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}" data-block-type="video" data-block-props="${videoBlockProps}">
|
|
1061
1245
|
<tr>
|
|
1062
1246
|
<td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
|
|
1063
|
-
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1247
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1064
1248
|
align="${style?.textAlign || "left"}"
|
|
1065
1249
|
style="
|
|
1066
1250
|
margin:0;
|
|
@@ -1140,16 +1324,6 @@ async function convertShapeBlock(blockData) {
|
|
|
1140
1324
|
justify: "justify",
|
|
1141
1325
|
};
|
|
1142
1326
|
const textAlignStyle = textAlignMap[textAlign] || "center";
|
|
1143
|
-
const flexJustify = textAlign === "left"
|
|
1144
|
-
? "flex-start"
|
|
1145
|
-
: textAlign === "right"
|
|
1146
|
-
? "flex-end"
|
|
1147
|
-
: "center";
|
|
1148
|
-
const flexAlign = verticalAlign === "top"
|
|
1149
|
-
? "flex-start"
|
|
1150
|
-
: verticalAlign === "bottom"
|
|
1151
|
-
? "flex-end"
|
|
1152
|
-
: "center";
|
|
1153
1327
|
// --- Text styling ---
|
|
1154
1328
|
const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;color:${color};`;
|
|
1155
1329
|
// ============================
|
|
@@ -1162,13 +1336,22 @@ async function convertShapeBlock(blockData) {
|
|
|
1162
1336
|
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1163
1337
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1164
1338
|
border-radius:${resolvedBorderRadius};
|
|
1165
|
-
background:${finalBackgroundColor}
|
|
1339
|
+
background-color:${finalBackgroundColor};
|
|
1340
|
+
background-image:url('${imageUrl}');
|
|
1341
|
+
background-position:center center;
|
|
1342
|
+
background-size:cover;
|
|
1343
|
+
background-repeat:no-repeat;
|
|
1166
1344
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1167
|
-
<
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1345
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
|
|
1346
|
+
style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
|
|
1347
|
+
<tr>
|
|
1348
|
+
<td align="${textAlignStyle}" valign="${verticalAlign}"
|
|
1349
|
+
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
1350
|
+
style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
|
|
1351
|
+
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
|
|
1352
|
+
</td>
|
|
1353
|
+
</tr>
|
|
1354
|
+
</table>
|
|
1172
1355
|
</div>`;
|
|
1173
1356
|
}
|
|
1174
1357
|
// --- Case 2: Image only ---
|
|
@@ -1187,15 +1370,20 @@ async function convertShapeBlock(blockData) {
|
|
|
1187
1370
|
else {
|
|
1188
1371
|
nonMsoContent = `
|
|
1189
1372
|
<div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
|
|
1190
|
-
background:${finalBackgroundColor};
|
|
1373
|
+
background-color:${finalBackgroundColor};
|
|
1191
1374
|
border:${borderWidth}px ${borderStyle} ${borderColor};
|
|
1192
1375
|
border-radius:${resolvedBorderRadius};
|
|
1193
1376
|
overflow:hidden;${alignmentStyle}${customCss || ""}">
|
|
1194
|
-
<
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1377
|
+
<table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
|
|
1378
|
+
style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
|
|
1379
|
+
<tr>
|
|
1380
|
+
<td align="${textAlignStyle}" valign="${verticalAlign}"
|
|
1381
|
+
width="${resolvedWidthPx}" height="${resolvedHeightPx}"
|
|
1382
|
+
style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
|
|
1383
|
+
<div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
|
|
1384
|
+
</td>
|
|
1385
|
+
</tr>
|
|
1386
|
+
</table>
|
|
1199
1387
|
</div>`;
|
|
1200
1388
|
}
|
|
1201
1389
|
// Outlook (VML) fallback
|
|
@@ -1377,7 +1565,7 @@ function convertVerticalDividerBlockToHtml(blockData) {
|
|
|
1377
1565
|
});
|
|
1378
1566
|
return `
|
|
1379
1567
|
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
1380
|
-
style="${exports.tableCommonStyle} max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
|
|
1568
|
+
style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
|
|
1381
1569
|
<tr>
|
|
1382
1570
|
<td style="${outerStyles}; text-align:center; vertical-align:middle;">
|
|
1383
1571
|
<!--[if mso | IE]>
|