@seliseblocks/mailcraft 0.2.7 → 0.2.8
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/mailcraft-editor.bundle.js +74 -48
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/package.json +1 -1
- package/src/core/blocks.js +15 -1
- package/src/core/editor-core.js +31 -1
- package/src/core/export.js +195 -6
- package/src/core/import-html.js +330 -20
- package/src/core/sanitize.js +8 -1
- package/src/render/block-body.js +109 -12
- package/src/render/canvas.js +45 -3
package/package.json
CHANGED
package/src/core/blocks.js
CHANGED
|
@@ -60,7 +60,14 @@ export const mkRow = (spans, blocks) => ({
|
|
|
60
60
|
border: 0, borderStyle: 'solid', lineColor: '#e2e2e5', bTop: true, bRight: true, bBottom: true, bLeft: true, radius: 0, shadow: '', maxW: 100,
|
|
61
61
|
mt: 0, mr: 0, mb: 0, ml: 0,
|
|
62
62
|
layout: 'columns', flexDir: 'row', justify: 'flex-start', alignItems: 'stretch', wrap: true, gridCols: 2,
|
|
63
|
-
py: 20, px: 24, padSplit: false, gap: 20, valign: 'top',
|
|
63
|
+
py: 20, px: 24, padSplit: false, gap: 20, valign: 'top',
|
|
64
|
+
// What this row does on a narrow screen. `mobileCols` is the number of
|
|
65
|
+
// columns it keeps there -- 1 stacks (the old `stackMobile: true`), 2 makes
|
|
66
|
+
// a two-up grid, 'keep' leaves the desktop layout alone (the old
|
|
67
|
+
// `stackMobile: false`). `mobileOrder: 'reverse'` flips the visual order,
|
|
68
|
+
// which is how an alternating image/text strip keeps the image on top of
|
|
69
|
+
// every band once stacked. Saved documents are mapped over in migrateDoc.
|
|
70
|
+
mobileCols: 1, mobileOrder: 'normal',
|
|
64
71
|
},
|
|
65
72
|
cols: spans.map((s, i) => ({ id: uid(), span: s, blocks: i === 0 && blocks ? blocks : [] })),
|
|
66
73
|
});
|
|
@@ -210,6 +217,13 @@ export function migrateDoc(doc) {
|
|
|
210
217
|
// 'square' choice rather than silently becoming an unrecognized value.
|
|
211
218
|
c.blocks.forEach((b) => { if (b.type === 'social' && b.props.shape === 'solid') b.props.shape = 'square'; });
|
|
212
219
|
});
|
|
220
|
+
// Mobile behaviour used to be one boolean, `stackMobile`. Mapped before
|
|
221
|
+
// the defaults are applied, or every document saved before this build
|
|
222
|
+
// would take `mobileCols: 1` from the defaults and a row that had
|
|
223
|
+
// deliberately opted out of stacking would start stacking.
|
|
224
|
+
if (r.props.mobileCols === undefined && r.props.stackMobile !== undefined) {
|
|
225
|
+
r.props.mobileCols = r.props.stackMobile === false ? 'keep' : 1;
|
|
226
|
+
}
|
|
213
227
|
Object.keys(defaults).forEach((k) => { if (r.props[k] === undefined) r.props[k] = defaults[k]; });
|
|
214
228
|
if (hadBlocks && !r.cols.some((c) => c.blocks.length)) emptied.push(r.id);
|
|
215
229
|
});
|
package/src/core/editor-core.js
CHANGED
|
@@ -1305,6 +1305,17 @@ export class EditorCore {
|
|
|
1305
1305
|
// line and a dead divider strip under the title.
|
|
1306
1306
|
const base = [];
|
|
1307
1307
|
const padF = [B.head('Spacing'), group(null, [B.range('Above & below', 'py', 0, 160, 2, 'px'), B.range('Left & right', 'px', 0, 120, 2, 'px')])];
|
|
1308
|
+
// Appended to every block type at once rather than repeated across
|
|
1309
|
+
// twenty switch arms. An absent `vis` means "all devices", so the
|
|
1310
|
+
// property only ever appears in a document that asked for it and no
|
|
1311
|
+
// migration is needed. Re-decorating an already-decorated list is safe:
|
|
1312
|
+
// `decorate` derives its flags from `kind`, which survives the pass.
|
|
1313
|
+
const visF = [B.head('Visibility'), B.seg('Show on', 'vis', [
|
|
1314
|
+
{ value: 'all', label: 'All' },
|
|
1315
|
+
{ value: 'desktop', label: 'Desktop' },
|
|
1316
|
+
{ value: 'mobile', label: 'Mobile' },
|
|
1317
|
+
])];
|
|
1318
|
+
const built = (() => {
|
|
1308
1319
|
switch (b.type) {
|
|
1309
1320
|
case 'text': return decorate(base.concat(
|
|
1310
1321
|
/<a(?:\s|>)/i.test(String(b.props.html || ''))
|
|
@@ -1368,6 +1379,11 @@ export class EditorCore {
|
|
|
1368
1379
|
case 'codeblock': return decorate(base.concat([B.area('Code', 'code'), B.color('Background', 'bg'), B.color('Text color', 'color'), B.range('Size', 'size', 8, 32, 0.5, 'px'), B.range('Padding', 'pad', 0, 80, 2, 'px')]));
|
|
1369
1380
|
default: return [];
|
|
1370
1381
|
}
|
|
1382
|
+
})();
|
|
1383
|
+
// Logic markers are editor furniture with no rendered body, so there is
|
|
1384
|
+
// nothing for a device to show or hide.
|
|
1385
|
+
if (b.type === 'condition' || b.type === 'loop') return built;
|
|
1386
|
+
return decorate(built.concat(visF));
|
|
1371
1387
|
}
|
|
1372
1388
|
if (f.row) {
|
|
1373
1389
|
const r = f.row;
|
|
@@ -1405,7 +1421,21 @@ export class EditorCore {
|
|
|
1405
1421
|
},
|
|
1406
1422
|
...(r.cols.length > 1 ? [
|
|
1407
1423
|
B.range('Space between columns', 'gap', 0, 120, 2, 'px'),
|
|
1408
|
-
B.
|
|
1424
|
+
B.head('On mobile'),
|
|
1425
|
+
// Replaces the old "Stack columns on mobile" switch, which could
|
|
1426
|
+
// only say all-or-nothing. Saved documents are mapped onto these
|
|
1427
|
+
// values in migrateDoc, so an existing toggle keeps its meaning.
|
|
1428
|
+
B.seg('Columns', 'mobileCols', [
|
|
1429
|
+
{ value: 1, label: 'One' },
|
|
1430
|
+
{ value: 2, label: 'Two' },
|
|
1431
|
+
{ value: 'keep', label: 'Keep' },
|
|
1432
|
+
]),
|
|
1433
|
+
// Only offered where it changes something: reversing a row that
|
|
1434
|
+
// keeps its desktop layout would do nothing.
|
|
1435
|
+
...(p.mobileCols !== 'keep' ? [B.seg('Order', 'mobileOrder', [
|
|
1436
|
+
{ value: 'normal', label: 'Normal' },
|
|
1437
|
+
{ value: 'reverse', label: 'Reverse' },
|
|
1438
|
+
])] : []),
|
|
1409
1439
|
] : []),
|
|
1410
1440
|
// Per-column styling, only for multi-column sections (a single
|
|
1411
1441
|
// column's background is just the section background).
|
package/src/core/export.js
CHANGED
|
@@ -48,6 +48,57 @@ function logicPlan(doc) {
|
|
|
48
48
|
return { emit, tail };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The two Word-engine declarations that have to be applied to the finished
|
|
53
|
+
* markup rather than to the DOM the renderer builds.
|
|
54
|
+
*
|
|
55
|
+
* `mso-*` are not real CSS properties, and CSSOM silently drops anything it
|
|
56
|
+
* does not recognise -- `style.msoLineHeightRule = 'exactly'` and
|
|
57
|
+
* `setProperty('mso-line-height-rule', ...)` both no-op, and the canvas is
|
|
58
|
+
* built through `Object.assign(node.style, ...)`. Since the exporter reads
|
|
59
|
+
* that DOM back as `outerHTML`, a string pass over the result is the only
|
|
60
|
+
* place these can be added. They are also meaningless anywhere but Outlook,
|
|
61
|
+
* so the canvas is better off without them.
|
|
62
|
+
*
|
|
63
|
+
* - `mso-line-height-rule:exactly` -- Classic Outlook otherwise ignores
|
|
64
|
+
* `line-height` outright and sets text solid. Added only where a real
|
|
65
|
+
* line-height is already declared, so it never invents spacing of its own.
|
|
66
|
+
* - `mso-table-lspace/rspace:0pt` -- Word adds its own horizontal space
|
|
67
|
+
* around a table, which shows up as phantom gaps between columns.
|
|
68
|
+
*
|
|
69
|
+
* Both are idempotent: a second pass finds its own marker and skips.
|
|
70
|
+
*/
|
|
71
|
+
export function msoHarden(html) {
|
|
72
|
+
return html
|
|
73
|
+
.replace(/style="([^"]*line-height:[^"]*)"/g, (m0, css) => {
|
|
74
|
+
if (/mso-line-height-rule/.test(css)) return m0;
|
|
75
|
+
// A ratio resolved against the font size in the same declaration.
|
|
76
|
+
// `exactly` tells Word to use the line-height verbatim, and a unitless
|
|
77
|
+
// 1.65 is not a length -- the pair is ambiguous at best and collapses
|
|
78
|
+
// the leading at worst. Every block that sets a line-height sets its
|
|
79
|
+
// font-size beside it, so the multiplication is exact rather than a
|
|
80
|
+
// guess; anything the renderer did not author (imported markup with a
|
|
81
|
+
// bare ratio, or a keyword like `normal`) is left exactly as it is and
|
|
82
|
+
// gets no `exactly` either, since there would be no length to honour.
|
|
83
|
+
const fs = css.match(/font-size:\s*([\d.]+)px/);
|
|
84
|
+
const out = fs
|
|
85
|
+
? css.replace(/line-height:\s*([\d.]+)\s*(;|$)/g, (s0, ratio, end) => 'line-height:' + Math.round(parseFloat(fs[1]) * parseFloat(ratio)) + 'px' + end)
|
|
86
|
+
: css;
|
|
87
|
+
if (!/line-height:\s*[\d.]+px/.test(out)) return 'style="' + out + '"';
|
|
88
|
+
return 'style="' + out + (out.trim().endsWith(';') ? '' : ';') + 'mso-line-height-rule:exactly;"';
|
|
89
|
+
})
|
|
90
|
+
.replace(/<table\b([^>]*)>/g, (m0, attrs) => {
|
|
91
|
+
if (/mso-table-lspace/.test(attrs)) return m0;
|
|
92
|
+
const spacing = 'mso-table-lspace:0pt;mso-table-rspace:0pt;';
|
|
93
|
+
// Appended, never prepended: the declarations the template actually
|
|
94
|
+
// authored stay at the front of the attribute, where both the importer
|
|
95
|
+
// and a human reading the source expect to find them.
|
|
96
|
+
return /style="/.test(attrs)
|
|
97
|
+
? '<table' + attrs.replace(/style="([^"]*)"/, (s0, css) => 'style="' + css + (!css.trim() || css.trim().endsWith(';') ? '' : ';') + spacing + '"') + '>'
|
|
98
|
+
: '<table' + attrs + ' style="' + spacing + '">';
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
51
102
|
/**
|
|
52
103
|
* Display-only dressing for the Code modal's live-preview iframe: the
|
|
53
104
|
* exported template carries literal {{#if}}/{{#each}} tags, which an iframe
|
|
@@ -102,8 +153,50 @@ export function buildHtml(state, root, boxCss) {
|
|
|
102
153
|
.replace(/\sdata-(?:gramm|gramm_editor|enable-grammarly|lt-active)="[^"]*"/g, '')
|
|
103
154
|
.replace(/\sdraggable="[^"]*"/g, '');
|
|
104
155
|
};
|
|
156
|
+
/*
|
|
157
|
+
* Which mobile rules this document actually needs. Only the ones a row (or
|
|
158
|
+
* a block) asks for are emitted, and rows that ask for the same thing share
|
|
159
|
+
* one class -- so a template that never leaves the defaults ships exactly
|
|
160
|
+
* the stylesheet it shipped before this feature existed, and one that uses
|
|
161
|
+
* every mode still ships a handful of rules rather than BEE's one selector
|
|
162
|
+
* per block with no deduplication.
|
|
163
|
+
*/
|
|
164
|
+
const need = { stack: false, twoUp: false, reverse: false, hideM: false, hideD: false };
|
|
165
|
+
/**
|
|
166
|
+
* One decision per row, made once: what goes on the row box (`<tr>`, or the
|
|
167
|
+
* flex/grid wrapper) and what goes on each cell.
|
|
168
|
+
*
|
|
169
|
+
* The plain one-up stack keeps the exact markup and rule it had before this
|
|
170
|
+
* feature -- `mc-col` on the cells, no flex anywhere -- so the overwhelmingly
|
|
171
|
+
* common case cannot regress, and if a sanitiser ever strips `display:flex`
|
|
172
|
+
* the fancy modes degrade to the desktop layout rather than taking ordinary
|
|
173
|
+
* stacking down with them.
|
|
174
|
+
*/
|
|
175
|
+
const mobilePlan = (rp, cols) => {
|
|
176
|
+
const mode = rp.mobileCols === undefined ? 1 : rp.mobileCols;
|
|
177
|
+
if (cols < 2 || mode === 'keep') return { row: '', cell: '' };
|
|
178
|
+
const rowCls = [];
|
|
179
|
+
// Two-up and reverse both need the row to become a flex container, which
|
|
180
|
+
// is safe precisely because it only ever runs inside the media query: a
|
|
181
|
+
// client that cannot do flex is a client that ignored the query and is
|
|
182
|
+
// still being shown the desktop table.
|
|
183
|
+
if (String(mode) === '2') { rowCls.push('mc-2up'); need.twoUp = true; }
|
|
184
|
+
if (rp.mobileOrder === 'reverse') { rowCls.push('mc-rev'); need.reverse = true; }
|
|
185
|
+
// Cells only carry `mc-col` in one-up; in two-up the row's own rule sizes
|
|
186
|
+
// them, so the two never fight over width.
|
|
187
|
+
const cell = String(mode) === '2' ? '' : 'mc-col';
|
|
188
|
+
if (cell) need.stack = true;
|
|
189
|
+
return { row: rowCls.join(' '), cell };
|
|
190
|
+
};
|
|
191
|
+
/** Per-block device visibility. Absent means "all devices", so nothing is emitted. */
|
|
192
|
+
const visClass = (bp) => {
|
|
193
|
+
if (bp.vis === 'desktop') { need.hideM = true; return ' class="mc-only-d"'; }
|
|
194
|
+
if (bp.vis === 'mobile') { need.hideD = true; return ' class="mc-only-m"'; }
|
|
195
|
+
return '';
|
|
196
|
+
};
|
|
105
197
|
const rows = d.rows.map((r) => {
|
|
106
198
|
const rp = r.props;
|
|
199
|
+
const plan = mobilePlan(rp, r.cols.length);
|
|
107
200
|
// A row holding nothing but logic markers exists to wrap the *sections*
|
|
108
201
|
// around it (drop a Condition onto the canvas above and below a group of
|
|
109
202
|
// rows). Emitting its <tr> scaffolding would leave an empty padded band
|
|
@@ -119,7 +212,7 @@ export function buildHtml(state, root, boxCss) {
|
|
|
119
212
|
if (b.type === 'html') return b.props.code || '';
|
|
120
213
|
if (b.type === 'svg') return '<div style="text-align:' + b.props.align + ';padding:' + b.props.py + 'px 0">' + (b.props.code || '') + '</div>';
|
|
121
214
|
if (b.type === 'condition' || b.type === 'loop') return logic.emit.get(b.id) || '';
|
|
122
|
-
return '<div style="' + boxCss(b.props) + '">' + grab(b.id) + '</div>';
|
|
215
|
+
return '<div' + visClass(b.props) + ' style="' + boxCss(b.props) + '">' + grab(b.id) + '</div>';
|
|
123
216
|
}).filter(Boolean).join('\n ') || ' ';
|
|
124
217
|
const cells = r.cols.map((c) => {
|
|
125
218
|
// Column-level styling (bg/radius/inner padding) renders as a wrapper
|
|
@@ -128,14 +221,21 @@ export function buildHtml(state, root, boxCss) {
|
|
|
128
221
|
const inner = (c.bg || c.border || c.radius || c.padY || c.padX)
|
|
129
222
|
? '<div style="background:' + (c.bg || 'transparent') + ';' + (c.border ? 'border:' + c.border + 'px ' + (c.borderStyle || 'solid') + ' ' + (c.lineColor || '#e2e2e5') + ';' : '') + 'border-radius:' + (c.radius || 0) + 'px;padding:' + (c.padY || 0) + 'px ' + (c.padX || 0) + 'px">\n ' + colInner(c) + '\n </div>'
|
|
130
223
|
: colInner(c);
|
|
131
|
-
return '<td width="' + c.span + '%" valign="' + rp.valign + '" style="padding:0 ' + Math.round(rp.gap / 2) + 'px;">\n ' + inner + '\n </td>';
|
|
224
|
+
return '<td' + (plan.cell ? ' class="' + plan.cell + '"' : '') + ' width="' + c.span + '%" valign="' + rp.valign + '" style="padding:0 ' + Math.round(rp.gap / 2) + 'px;">\n ' + inner + '\n </td>';
|
|
132
225
|
}).join('\n ');
|
|
226
|
+
// The CSS-layout rows reach the same behaviour through their wrapper: one
|
|
227
|
+
// class on the flex/grid container, so the markup stays exactly as it was
|
|
228
|
+
// for every wide client.
|
|
229
|
+
const wrapCls = [plan.cell ? 'mc-stack' : '', plan.row].filter(Boolean).join(' ');
|
|
230
|
+
const stackWrap = wrapCls ? ' class="' + wrapCls + '"' : '';
|
|
133
231
|
const cssBody = rp.layout === 'grid'
|
|
134
|
-
? '<div style="display:grid;grid-template-columns:repeat(' + (rp.gridCols || 2) + ',minmax(0,1fr));gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div>' + colInner(c) + '</div>').join('\n ') + '\n </div>'
|
|
135
|
-
: '<div style="display:flex;flex-direction:' + (rp.flexDir || 'row') + ';justify-content:' + (rp.justify || 'flex-start') + ';align-items:' + (rp.alignItems || 'stretch') + ';flex-wrap:' + (rp.wrap ? 'wrap' : 'nowrap') + ';gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div style="flex:' + c.span + ' 1 auto;min-width:0">' + colInner(c) + '</div>').join('\n ') + '\n </div>';
|
|
232
|
+
? '<div' + stackWrap + ' style="display:grid;grid-template-columns:repeat(' + (rp.gridCols || 2) + ',minmax(0,1fr));gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div>' + colInner(c) + '</div>').join('\n ') + '\n </div>'
|
|
233
|
+
: '<div' + stackWrap + ' style="display:flex;flex-direction:' + (rp.flexDir || 'row') + ';justify-content:' + (rp.justify || 'flex-start') + ';align-items:' + (rp.alignItems || 'stretch') + ';flex-wrap:' + (rp.wrap ? 'wrap' : 'nowrap') + ';gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div style="flex:' + c.span + ' 1 auto;min-width:0">' + colInner(c) + '</div>').join('\n ') + '\n </div>';
|
|
136
234
|
const body = rp.layout && rp.layout !== 'columns'
|
|
137
235
|
? cssBody
|
|
138
|
-
|
|
236
|
+
// The `<tr>` is what becomes the flex container for two-up and reverse;
|
|
237
|
+
// in the default one-up stack it carries no class at all, exactly as before.
|
|
238
|
+
: '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr' + (plan.row ? ' class="' + plan.row + '"' : '') + '>\n ' + cells + '\n </tr></table>';
|
|
139
239
|
const tdBg = rp.bgImage
|
|
140
240
|
? 'background-color:' + (rp.bg || t.contentBg || 'transparent') + ';background-image:' + (rp.overlay ? 'linear-gradient(rgba(20,22,24,' + (rp.overlay / 100) + '),rgba(20,22,24,' + (rp.overlay / 100) + ')),' : '') + 'url("' + cssUrl(rp.bgImage) + '");background-size:' + (rp.bgSize || 'cover') + ';background-position:' + (rp.bgPos || 'center') + ';background-repeat:' + (rp.bgRepeat || 'no-repeat') + ';'
|
|
141
241
|
// Falls all the way through to `transparent`: a row inherits the
|
|
@@ -165,5 +265,94 @@ export function buildHtml(state, root, boxCss) {
|
|
|
165
265
|
// Patchy in mail clients (Outlook drops it), but honest: what the user
|
|
166
266
|
// styled ships, and capable clients render it.
|
|
167
267
|
+ (t.shadow ? 'box-shadow:' + t.shadow + ';' : '');
|
|
168
|
-
|
|
268
|
+
/*
|
|
269
|
+
* The content column is `width:100%` capped by `max-width`, never a fixed
|
|
270
|
+
* `width:<n>px`.
|
|
271
|
+
*
|
|
272
|
+
* The fixed width was what made the sent email unresponsive, and not only
|
|
273
|
+
* for itself: a px width becomes the table's min-content contribution, so
|
|
274
|
+
* it propagated outward and pinned the full-width wrapper open too. A plain
|
|
275
|
+
* text-only template measured 620px of horizontal scroll on a 390px phone,
|
|
276
|
+
* and `max-width:100%` -- already sitting right there -- never got the
|
|
277
|
+
* chance to engage, because 100% of a container the table had itself forced
|
|
278
|
+
* to 620px is 620px. Swapping the two makes the cap the real constraint:
|
|
279
|
+
* measured 500px (fits) on the phone, unchanged 620px on the desktop.
|
|
280
|
+
*
|
|
281
|
+
* The `<!--[if mso]>` pair is the price of that. Word-based Outlook honours
|
|
282
|
+
* neither `max-width` nor the media query below, so on its own it would now
|
|
283
|
+
* render the column edge to edge across the whole window; the ghost table
|
|
284
|
+
* is a fixed-width cage only Outlook sees, which holds the old geometry for
|
|
285
|
+
* exactly the client that cannot do better. Every other client skips the
|
|
286
|
+
* conditional comment entirely and gets the fluid table.
|
|
287
|
+
*/
|
|
288
|
+
const ghostOpen = '<!--[if mso]><table role="presentation" width="' + t.width + '" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->';
|
|
289
|
+
const ghostClose = '<!--[if mso]></td></tr></table><![endif]-->';
|
|
290
|
+
const shell = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:' + t.width + 'px;background:' + (t.contentBg || 'transparent') + ';' + contentShape + '">\n' + rows + '\n </table>';
|
|
291
|
+
/*
|
|
292
|
+
* The one embedded stylesheet in the document, and the only place the
|
|
293
|
+
* exporter is not inline-styled -- a media query cannot be expressed
|
|
294
|
+
* inline, and every rule here is a narrow-screen override of an inline
|
|
295
|
+
* style, hence `!important` throughout.
|
|
296
|
+
*
|
|
297
|
+
* `mc-col` / `mc-stack` are what finally make "Stack columns on mobile" do
|
|
298
|
+
* something: the toggle has shipped in the row inspector since columns
|
|
299
|
+
* existed, but nothing read the prop -- no media query was emitted and the
|
|
300
|
+
* canvas ignored it -- so a 4-column row stayed 4 columns of 60px on a
|
|
301
|
+
* phone. The image rule is the other half of being responsive: a fixed-width
|
|
302
|
+
* image from an import would otherwise hold a stacked column open.
|
|
303
|
+
*/
|
|
304
|
+
const stackCss = '\n<style>\n'
|
|
305
|
+
+ '/* Apple Mail and iOS auto-detect dates, addresses and phone numbers and\n'
|
|
306
|
+
+ ' repaint them as blue underlined links. This hands them back to the\n'
|
|
307
|
+
+ ' surrounding text; links the template actually declares are untouched,\n'
|
|
308
|
+
+ ' because they carry their own inline colour. */\n'
|
|
309
|
+
+ 'a[x-apple-data-detectors] { color:inherit !important; text-decoration:none !important; }\n'
|
|
310
|
+
/*
|
|
311
|
+
* No `p { line-height: inherit }` here, though it is standard in
|
|
312
|
+
* hand-written email and BEE emits it. This document is also an *input*:
|
|
313
|
+
* core/css-cascade.js folds every non-`@media` rule into inline styles on
|
|
314
|
+
* import, so that one rule came back stamped on every paragraph and an
|
|
315
|
+
* export -> import -> export cycle stopped converging. The pixel
|
|
316
|
+
* line-heights it would have protected are already inline on the block
|
|
317
|
+
* that owns them, which is the stronger guarantee anyway.
|
|
318
|
+
*/
|
|
319
|
+
// A mobile-only block has to be hidden here, outside the query, because
|
|
320
|
+
// Classic Outlook never reads the query -- `display:none` alone would
|
|
321
|
+
// leave it visible in exactly the client that cannot be told otherwise.
|
|
322
|
+
// `mso-hide` is the half Word understands; the rest is for everyone else.
|
|
323
|
+
+ (need.hideD ? '.mc-only-m, .mc-only-m table { mso-hide:all; display:none; max-height:0; overflow:hidden; }\n' : '')
|
|
324
|
+
+ '@media only screen and (max-width:' + t.width + 'px) {\n'
|
|
325
|
+
+ (need.stack ? ' .mc-col { display:block !important; width:100% !important; padding-left:0 !important; padding-right:0 !important; }\n'
|
|
326
|
+
+ ' .mc-stack { display:block !important; }\n'
|
|
327
|
+
+ ' .mc-stack > div { width:100% !important; }\n' : '')
|
|
328
|
+
// Two-up: the row becomes a flex container and each cell takes half.
|
|
329
|
+
// `box-sizing` is load-bearing -- with the default content box, a cell's
|
|
330
|
+
// own padding pushes 50% over the line and every cell wraps to its own row.
|
|
331
|
+
+ (need.twoUp ? ' .mc-2up { display:flex !important; flex-wrap:wrap !important; }\n'
|
|
332
|
+
+ ' .mc-2up > td, .mc-2up > div { box-sizing:border-box !important; flex:0 0 50% !important; max-width:50% !important; }\n' : '')
|
|
333
|
+
// Reverse: `column-reverse` flips a stack of any depth, where the usual
|
|
334
|
+
// `table-header-group` trick has only two usable slots and so cannot
|
|
335
|
+
// reverse a three- or four-column row at all. Two-up reverses along both
|
|
336
|
+
// axes so the last cell ends up first.
|
|
337
|
+
+ (need.reverse ? ' .mc-rev { display:flex !important; flex-wrap:wrap !important; flex-direction:column-reverse !important; }\n'
|
|
338
|
+
+ ' .mc-2up.mc-rev { flex-direction:row-reverse !important; flex-wrap:wrap-reverse !important; }\n' : '')
|
|
339
|
+
+ (need.hideM ? ' .mc-only-d, .mc-only-d table { display:none !important; max-height:0 !important; overflow:hidden !important; }\n' : '')
|
|
340
|
+
+ (need.hideD ? ' .mc-only-m, .mc-only-m table { display:block !important; max-height:none !important; overflow:visible !important; }\n' : '')
|
|
341
|
+
+ ' img { max-width:100% !important; height:auto !important; }\n'
|
|
342
|
+
+ '}\n</style>';
|
|
343
|
+
/*
|
|
344
|
+
* `xmlns:o` earns its place; `xmlns:v` does not. The Office namespace is
|
|
345
|
+
* what makes `<o:OfficeDocumentSettings>` parse, and `PixelsPerInch` 96 is
|
|
346
|
+
* a live bug fix rather than a legacy one: on a high-DPI Windows display
|
|
347
|
+
* Outlook renders at 120dpi and scales the whole template about 25% larger
|
|
348
|
+
* than authored. VML's namespace is deliberately absent -- nothing here
|
|
349
|
+
* emits VML, and declaring a namespace for markup that never appears is
|
|
350
|
+
* noise in every other client.
|
|
351
|
+
*/
|
|
352
|
+
const msoHead = '\n<!--[if mso]>\n<xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>\n<![endif]-->';
|
|
353
|
+
// `text-size-adjust` at 100%, never `none`: both stop a mobile client
|
|
354
|
+
// inflating the type, but `none` also blocks legitimate scaling and leaves
|
|
355
|
+
// text unreadably small on some Android clients.
|
|
356
|
+
const bodyStyle = 'margin:0;padding:0;background:' + pageBg + ';font-family:' + t.font.replace(/"/g, "'") + ';color:' + t.text + ';-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;text-size-adjust:100%;';
|
|
357
|
+
return msoHarden('<!doctype html>\n<html lang="en" xmlns:o="urn:schemas-microsoft-com:office:office">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<meta name="color-scheme" content="light">\n<meta name="supported-color-schemes" content="light">\n<title>' + 'Email' + '</title>' + msoHead + stackCss + '\n</head>\n<body style="' + bodyStyle + '">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:' + pageBg + ';">\n <tr><td align="center" style="padding:' + pagePad + ';">\n ' + ghostOpen + '\n ' + shell + '\n ' + ghostClose + '\n </td></tr>\n</table>\n</body>\n</html>');
|
|
169
358
|
}
|