@seliseblocks/mailcraft 0.2.13 → 0.2.15
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/CHANGELOG.md +16 -0
- package/DOCS.md +5 -4
- package/README.md +2 -1
- package/README.md.txt +2 -1
- package/dist/mailcraft-editor.bundle.js +72 -72
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/back-in-stock.html +1 -8
- package/examples/templates/cart-left-behind.html +1 -8
- package/examples/templates/meet-nova-launch.html +5 -0
- package/examples/templates/rate-your-headphones.html +1 -8
- package/examples/templates/the-sunday-brief.html +5 -0
- package/examples/templates/your-order-shipped.html +1 -0
- package/examples/vanilla.html +14 -24
- package/package.json +1 -1
- package/src/core/editor-core.js +116 -1
- package/src/core/i18n/ar.js +2 -1
- package/src/core/i18n/bg.js +2 -1
- package/src/core/i18n/bn.js +2 -1
- package/src/core/i18n/ca.js +2 -1
- package/src/core/i18n/cs.js +2 -1
- package/src/core/i18n/da.js +2 -1
- package/src/core/i18n/de-CH.js +2 -1
- package/src/core/i18n/de.js +2 -1
- package/src/core/i18n/dz.js +2 -1
- package/src/core/i18n/el.js +2 -1
- package/src/core/i18n/en.js +2 -1
- package/src/core/i18n/es.js +2 -1
- package/src/core/i18n/et.js +2 -1
- package/src/core/i18n/fi.js +2 -1
- package/src/core/i18n/fr.js +2 -1
- package/src/core/i18n/hr.js +2 -1
- package/src/core/i18n/hu.js +2 -1
- package/src/core/i18n/index.js +83 -83
- package/src/core/i18n/it.js +2 -1
- package/src/core/i18n/lt.js +2 -1
- package/src/core/i18n/lv.js +2 -1
- package/src/core/i18n/nb.js +2 -1
- package/src/core/i18n/nl.js +2 -1
- package/src/core/i18n/pl.js +2 -1
- package/src/core/i18n/pt.js +2 -1
- package/src/core/i18n/ro.js +2 -1
- package/src/core/i18n/ru.js +2 -1
- package/src/core/i18n/sk.js +2 -1
- package/src/core/i18n/sl.js +2 -1
- package/src/core/i18n/sv.js +2 -1
- package/src/core/i18n/tr.js +2 -1
- package/src/core/i18n/uk.js +2 -1
- package/src/core/ids.js +1 -1
- package/src/core/layout-style.js +100 -100
- package/src/core/parse.js +10 -10
- package/src/core/placeholder.js +15 -15
- package/src/core/variables.js +11 -11
- package/src/mailcraft-editor.js +20 -8
- package/src/render/block-body.js +7 -1
- package/src/render/focus-preserve.js +158 -158
- package/src/render/rte.js +4 -1
- package/src/render/screenshot.js +81 -5
- package/src/render/story.js +513 -415
- package/types/index.d.ts +22 -7
package/src/core/layout-style.js
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure style-computation functions shared by the live renderer and the export
|
|
3
|
-
* builder, ported verbatim. Objects use camelCase keys so they can be applied
|
|
4
|
-
* directly via `Object.assign(el.style, obj)`.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { cssUrl } from './sanitize.js';
|
|
8
|
-
export function pad(p) {
|
|
9
|
-
return (p.py || 0) + 'px ' + (p.px || 0) + 'px';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function boxStyle(p) {
|
|
13
|
-
const on = (key) => p[key] !== false;
|
|
14
|
-
const side = (key) => (p.bBorder && on(key) ? p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5') : '0');
|
|
15
|
-
return {
|
|
16
|
-
background: p.bBg || 'transparent',
|
|
17
|
-
borderTop: side('bTop'), borderRight: side('bRight'), borderBottom: side('bBottom'), borderLeft: side('bLeft'),
|
|
18
|
-
borderRadius: (p.bRadius || 0) + 'px',
|
|
19
|
-
padding: (p.bPad || 0) + 'px',
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function boxCss(p) {
|
|
24
|
-
const bits = [];
|
|
25
|
-
if (p.bBg) bits.push('background:' + p.bBg);
|
|
26
|
-
if (p.bBorder) {
|
|
27
|
-
const value = p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5');
|
|
28
|
-
const sides = { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
|
|
29
|
-
if (sides.top && sides.right && sides.bottom && sides.left) bits.push('border:' + value);
|
|
30
|
-
else Object.keys(sides).filter((key) => sides[key]).forEach((key) => bits.push('border-' + key + ':' + value));
|
|
31
|
-
}
|
|
32
|
-
if (p.bRadius) bits.push('border-radius:' + p.bRadius + 'px');
|
|
33
|
-
if (p.bPad) bits.push('padding:' + p.bPad + 'px');
|
|
34
|
-
return bits.length ? bits.join(';') + ';' : 'margin:0';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A row's effective padding as a four-value CSS shorthand. `pt/pb/pl/pr` are optional per-side overrides (set by the inspector's "Per-side padding" split, or by the importer for asymmetric source padding); wherever a side is absent it follows the linked `py`/`px` pair, so documents that never split keep behaving exactly as before. */
|
|
1
|
+
/**
|
|
2
|
+
* Pure style-computation functions shared by the live renderer and the export
|
|
3
|
+
* builder, ported verbatim. Objects use camelCase keys so they can be applied
|
|
4
|
+
* directly via `Object.assign(el.style, obj)`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cssUrl } from './sanitize.js';
|
|
8
|
+
export function pad(p) {
|
|
9
|
+
return (p.py || 0) + 'px ' + (p.px || 0) + 'px';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function boxStyle(p) {
|
|
13
|
+
const on = (key) => p[key] !== false;
|
|
14
|
+
const side = (key) => (p.bBorder && on(key) ? p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5') : '0');
|
|
15
|
+
return {
|
|
16
|
+
background: p.bBg || 'transparent',
|
|
17
|
+
borderTop: side('bTop'), borderRight: side('bRight'), borderBottom: side('bBottom'), borderLeft: side('bLeft'),
|
|
18
|
+
borderRadius: (p.bRadius || 0) + 'px',
|
|
19
|
+
padding: (p.bPad || 0) + 'px',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function boxCss(p) {
|
|
24
|
+
const bits = [];
|
|
25
|
+
if (p.bBg) bits.push('background:' + p.bBg);
|
|
26
|
+
if (p.bBorder) {
|
|
27
|
+
const value = p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5');
|
|
28
|
+
const sides = { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
|
|
29
|
+
if (sides.top && sides.right && sides.bottom && sides.left) bits.push('border:' + value);
|
|
30
|
+
else Object.keys(sides).filter((key) => sides[key]).forEach((key) => bits.push('border-' + key + ':' + value));
|
|
31
|
+
}
|
|
32
|
+
if (p.bRadius) bits.push('border-radius:' + p.bRadius + 'px');
|
|
33
|
+
if (p.bPad) bits.push('padding:' + p.bPad + 'px');
|
|
34
|
+
return bits.length ? bits.join(';') + ';' : 'margin:0';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A row's effective padding as a four-value CSS shorthand. `pt/pb/pl/pr` are optional per-side overrides (set by the inspector's "Per-side padding" split, or by the importer for asymmetric source padding); wherever a side is absent it follows the linked `py`/`px` pair, so documents that never split keep behaving exactly as before. */
|
|
38
38
|
export function rowPad(p) {
|
|
39
|
-
const t = p.pt ?? p.py ?? 0;
|
|
40
|
-
const b = p.pb ?? p.py ?? 0;
|
|
41
|
-
const l = p.pl ?? p.px ?? 0;
|
|
42
|
-
const r = p.pr ?? p.px ?? 0;
|
|
43
|
-
return t + 'px ' + r + 'px ' + b + 'px ' + l + 'px';
|
|
39
|
+
const t = p.pt ?? p.py ?? 0;
|
|
40
|
+
const b = p.pb ?? p.py ?? 0;
|
|
41
|
+
const l = p.pl ?? p.px ?? 0;
|
|
42
|
+
const r = p.pr ?? p.px ?? 0;
|
|
43
|
+
return t + 'px ' + r + 'px ' + b + 'px ' + l + 'px';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** A row's outside spacing in CSS clockwise order, with the old vertical `my` value as a saved-document fallback. Empty horizontal margins can remain `auto` in the live canvas so the advanced max-width control stays centered. */
|
|
@@ -52,64 +52,64 @@ export function rowMargin(p, centerEmpty) {
|
|
|
52
52
|
const emptyHorizontal = centerEmpty && !r && !l;
|
|
53
53
|
return t + 'px ' + (emptyHorizontal ? 'auto' : r + 'px') + ' ' + b + 'px ' + (emptyHorizontal ? 'auto' : l + 'px');
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
/** Which sides a row's border draws on. Sides default ON (`!== false`) so documents saved before per-side toggles existed keep their full border. */
|
|
57
|
-
export function rowBorderSides(p) {
|
|
58
|
-
return { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The row border as an inline-CSS string (export path). Empty when the width is 0 or every side is toggled off. */
|
|
62
|
-
export function rowBorderCss(p) {
|
|
63
|
-
if (!p.border) return '';
|
|
64
|
-
const s = rowBorderSides(p);
|
|
65
|
-
const value = p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5');
|
|
66
|
-
if (s.top && s.right && s.bottom && s.left) return 'border:' + value + ';';
|
|
67
|
-
return ['top', 'right', 'bottom', 'left'].filter((k) => s[k]).map((k) => 'border-' + k + ':' + value + ';').join('');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function rowBg(p) {
|
|
71
|
-
const ov = (p.overlay || 0) / 100;
|
|
72
|
-
const layers = [];
|
|
73
|
-
if (p.bgImage && ov) layers.push('linear-gradient(rgba(20,22,24,' + ov + '),rgba(20,22,24,' + ov + '))');
|
|
74
|
-
if (p.bgImage) layers.push('url("' + cssUrl(p.bgImage) + '")');
|
|
75
|
-
const s = rowBorderSides(p);
|
|
76
|
-
const side = (on) => (p.border && on ? p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5') : '0');
|
|
77
|
-
return {
|
|
78
|
-
backgroundColor: p.bg || 'transparent',
|
|
79
|
-
backgroundImage: layers.length ? layers.join(',') : 'none',
|
|
80
|
-
backgroundSize: p.bgSize || 'cover',
|
|
81
|
-
backgroundPosition: p.bgPos || 'center',
|
|
82
|
-
backgroundRepeat: p.bgRepeat || 'no-repeat',
|
|
83
|
-
borderTop: side(s.top),
|
|
84
|
-
borderRight: side(s.right),
|
|
85
|
-
borderBottom: side(s.bottom),
|
|
86
|
-
borderLeft: side(s.left),
|
|
87
|
-
borderRadius: (p.radius || 0) + 'px',
|
|
88
|
-
// A raw CSS string, not a boolean: imports keep the source's exact
|
|
89
|
-
// shadow; the inspector toggle writes/clears a standard one.
|
|
90
|
-
boxShadow: p.shadow || 'none',
|
|
91
|
-
maxWidth: (p.maxW || 100) + '%',
|
|
55
|
+
|
|
56
|
+
/** Which sides a row's border draws on. Sides default ON (`!== false`) so documents saved before per-side toggles existed keep their full border. */
|
|
57
|
+
export function rowBorderSides(p) {
|
|
58
|
+
return { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The row border as an inline-CSS string (export path). Empty when the width is 0 or every side is toggled off. */
|
|
62
|
+
export function rowBorderCss(p) {
|
|
63
|
+
if (!p.border) return '';
|
|
64
|
+
const s = rowBorderSides(p);
|
|
65
|
+
const value = p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5');
|
|
66
|
+
if (s.top && s.right && s.bottom && s.left) return 'border:' + value + ';';
|
|
67
|
+
return ['top', 'right', 'bottom', 'left'].filter((k) => s[k]).map((k) => 'border-' + k + ':' + value + ';').join('');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function rowBg(p) {
|
|
71
|
+
const ov = (p.overlay || 0) / 100;
|
|
72
|
+
const layers = [];
|
|
73
|
+
if (p.bgImage && ov) layers.push('linear-gradient(rgba(20,22,24,' + ov + '),rgba(20,22,24,' + ov + '))');
|
|
74
|
+
if (p.bgImage) layers.push('url("' + cssUrl(p.bgImage) + '")');
|
|
75
|
+
const s = rowBorderSides(p);
|
|
76
|
+
const side = (on) => (p.border && on ? p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5') : '0');
|
|
77
|
+
return {
|
|
78
|
+
backgroundColor: p.bg || 'transparent',
|
|
79
|
+
backgroundImage: layers.length ? layers.join(',') : 'none',
|
|
80
|
+
backgroundSize: p.bgSize || 'cover',
|
|
81
|
+
backgroundPosition: p.bgPos || 'center',
|
|
82
|
+
backgroundRepeat: p.bgRepeat || 'no-repeat',
|
|
83
|
+
borderTop: side(s.top),
|
|
84
|
+
borderRight: side(s.right),
|
|
85
|
+
borderBottom: side(s.bottom),
|
|
86
|
+
borderLeft: side(s.left),
|
|
87
|
+
borderRadius: (p.radius || 0) + 'px',
|
|
88
|
+
// A raw CSS string, not a boolean: imports keep the source's exact
|
|
89
|
+
// shadow; the inspector toggle writes/clears a standard one.
|
|
90
|
+
boxShadow: p.shadow || 'none',
|
|
91
|
+
maxWidth: (p.maxW || 100) + '%',
|
|
92
92
|
margin: rowMargin(p, true),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
export function colsWrap(p) {
|
|
97
|
-
const gap = p.gap || 0;
|
|
98
|
-
if (p.layout === 'grid') return { display: 'grid', gridTemplateColumns: 'repeat(' + (p.gridCols || 2) + ', minmax(0, 1fr))', gap: gap + 'px' };
|
|
99
|
-
if (p.layout === 'flex') {
|
|
100
|
-
return {
|
|
101
|
-
display: 'flex', flexDirection: p.flexDir || 'row', justifyContent: p.justify || 'flex-start',
|
|
102
|
-
alignItems: p.alignItems || 'stretch', flexWrap: p.wrap ? 'wrap' : 'nowrap', gap: gap + 'px',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
return { display: 'flex', alignItems: 'stretch', margin: '0 ' + (-gap / 2) + 'px' };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function colStyle(p, c) {
|
|
109
|
-
if (p.layout === 'grid') return { minWidth: 0 };
|
|
110
|
-
if (p.layout === 'flex') return { flex: (p.flexDir || 'row').indexOf('column') === 0 ? '0 0 auto' : c.span + ' 1 auto', minWidth: 0 };
|
|
111
|
-
return {
|
|
112
|
-
flex: c.span + ' 1 0%', minWidth: 0, padding: '0 ' + (p.gap || 0) / 2 + 'px',
|
|
113
|
-
alignSelf: p.valign === 'middle' ? 'center' : (p.valign === 'bottom' ? 'flex-end' : 'flex-start'),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
95
|
+
|
|
96
|
+
export function colsWrap(p) {
|
|
97
|
+
const gap = p.gap || 0;
|
|
98
|
+
if (p.layout === 'grid') return { display: 'grid', gridTemplateColumns: 'repeat(' + (p.gridCols || 2) + ', minmax(0, 1fr))', gap: gap + 'px' };
|
|
99
|
+
if (p.layout === 'flex') {
|
|
100
|
+
return {
|
|
101
|
+
display: 'flex', flexDirection: p.flexDir || 'row', justifyContent: p.justify || 'flex-start',
|
|
102
|
+
alignItems: p.alignItems || 'stretch', flexWrap: p.wrap ? 'wrap' : 'nowrap', gap: gap + 'px',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { display: 'flex', alignItems: 'stretch', margin: '0 ' + (-gap / 2) + 'px' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function colStyle(p, c) {
|
|
109
|
+
if (p.layout === 'grid') return { minWidth: 0 };
|
|
110
|
+
if (p.layout === 'flex') return { flex: (p.flexDir || 'row').indexOf('column') === 0 ? '0 0 auto' : c.span + ' 1 auto', minWidth: 0 };
|
|
111
|
+
return {
|
|
112
|
+
flex: c.span + ' 1 0%', minWidth: 0, padding: '0 ' + (p.gap || 0) / 2 + 'px',
|
|
113
|
+
alignSelf: p.valign === 'middle' ? 'center' : (p.valign === 'bottom' ? 'flex-end' : 'flex-start'),
|
|
114
|
+
};
|
|
115
|
+
}
|
package/src/core/parse.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export function parseItems(s) {
|
|
2
|
-
return String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
3
|
-
const i = l.indexOf('|');
|
|
4
|
-
return i < 0 ? { label: l, href: '#' } : { label: l.slice(0, i).trim(), href: l.slice(i + 1).trim() };
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function cellsOf(p) {
|
|
9
|
-
return String(p.data || '').split('\n').filter((l) => l.trim()).map((l) => l.split('|').map((c) => c.trim()));
|
|
10
|
-
}
|
|
1
|
+
export function parseItems(s) {
|
|
2
|
+
return String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
3
|
+
const i = l.indexOf('|');
|
|
4
|
+
return i < 0 ? { label: l, href: '#' } : { label: l.slice(0, i).trim(), href: l.slice(i + 1).trim() };
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function cellsOf(p) {
|
|
9
|
+
return String(p.data || '').split('\n').filter((l) => l.trim()).map((l) => l.split('|').map((c) => c.trim()));
|
|
10
|
+
}
|
package/src/core/placeholder.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
/** Data-URI placeholder image generator, ported verbatim from the original. */
|
|
2
|
-
function enc(s) {
|
|
3
|
-
return encodeURIComponent(s).replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function PH(label, w, ht) {
|
|
7
|
-
return 'data:image/svg+xml;utf8,' + enc(
|
|
8
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">' +
|
|
9
|
-
'<defs><pattern id="s" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">' +
|
|
10
|
-
'<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>' +
|
|
11
|
-
'<rect width="100%" height="100%" fill="url(#s)"/>' +
|
|
12
|
-
'<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>' +
|
|
13
|
-
'<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="' + Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
|
|
14
|
-
);
|
|
15
|
-
}
|
|
1
|
+
/** Data-URI placeholder image generator, ported verbatim from the original. */
|
|
2
|
+
function enc(s) {
|
|
3
|
+
return encodeURIComponent(s).replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function PH(label, w, ht) {
|
|
7
|
+
return 'data:image/svg+xml;utf8,' + enc(
|
|
8
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">' +
|
|
9
|
+
'<defs><pattern id="s" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">' +
|
|
10
|
+
'<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>' +
|
|
11
|
+
'<rect width="100%" height="100%" fill="url(#s)"/>' +
|
|
12
|
+
'<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>' +
|
|
13
|
+
'<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="' + Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
|
|
14
|
+
);
|
|
15
|
+
}
|
package/src/core/variables.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export const DEFAULT_VARS = 'first_name\nlast_name\nemail\ncompany\ncity\norder_id\nplan\ndiscount\nunsubscribe_url';
|
|
2
|
-
export const TOKEN = (t) => '{' + '{ ' + t + ' }' + '}';
|
|
3
|
-
|
|
4
|
-
/** Variables are supplied by the host application -- the editor only ever shows the tokens, never a substituted value. */
|
|
5
|
-
export function vars(raw) {
|
|
6
|
-
const list = Array.isArray(raw) ? raw : String(raw == null ? DEFAULT_VARS : raw).split(/[\n,]/);
|
|
7
|
-
return list.map((v) => String(v).trim().replace(/^\{\{\s*|\s*\}\}$/g, '')).filter(Boolean);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Which prop field a merge tag lands in when inserted from the Data tab, keyed by the selected block's type. */
|
|
11
|
-
export const INSERT_KEYS = { text: 'html', heading: 'text', button: 'label', html: 'code', codeblock: 'code', quote: 'text', list: 'items', table: 'data' };
|
|
1
|
+
export const DEFAULT_VARS = 'first_name\nlast_name\nemail\ncompany\ncity\norder_id\nplan\ndiscount\nunsubscribe_url';
|
|
2
|
+
export const TOKEN = (t) => '{' + '{ ' + t + ' }' + '}';
|
|
3
|
+
|
|
4
|
+
/** Variables are supplied by the host application -- the editor only ever shows the tokens, never a substituted value. */
|
|
5
|
+
export function vars(raw) {
|
|
6
|
+
const list = Array.isArray(raw) ? raw : String(raw == null ? DEFAULT_VARS : raw).split(/[\n,]/);
|
|
7
|
+
return list.map((v) => String(v).trim().replace(/^\{\{\s*|\s*\}\}$/g, '')).filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Which prop field a merge tag lands in when inserted from the Data tab, keyed by the selected block's type. */
|
|
11
|
+
export const INSERT_KEYS = { text: 'html', heading: 'text', button: 'label', html: 'code', codeblock: 'code', quote: 'text', list: 'items', table: 'data' };
|
package/src/mailcraft-editor.js
CHANGED
|
@@ -96,8 +96,10 @@ function tip(node, label, dir, align) {
|
|
|
96
96
|
* a host's own table, an imported locale from `core/i18n/`, or both merged via
|
|
97
97
|
* `defineMessages`; see `core/i18n/index.js`).
|
|
98
98
|
* Methods: `getContent()`, `setContent(doc)`, `importHtml(html)`, `exportHtml()`,
|
|
99
|
-
* `loadTemplate(tpl)`, `screenshotPng()`
|
|
100
|
-
* (full-template
|
|
99
|
+
* `loadTemplate(tpl)`, `screenshotPng(options)`
|
|
100
|
+
* (full-template image as a Blob; `options.format`/`quality`/`scale` pick
|
|
101
|
+
* PNG, JPEG or WebP and the compression), `previewScreenshot()` (story-style
|
|
102
|
+
* viewer with its own format picker),
|
|
101
103
|
* `downloadScreenshot()`, `copyScreenshot()`, `undo()`, `redo()`.
|
|
102
104
|
* Events: `change` (detail: doc), `export` (detail: html string).
|
|
103
105
|
*/
|
|
@@ -473,20 +475,30 @@ export class MailCraftEditor extends ElementBase {
|
|
|
473
475
|
return html;
|
|
474
476
|
}
|
|
475
477
|
|
|
476
|
-
/**
|
|
477
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Full-template screenshot as an image Blob (2x resolution by default,
|
|
480
|
+
* desktop width, independent of the current device/zoom view). `options`
|
|
481
|
+
* is the compression dial: `{ format: 'png' | 'jpeg' | 'webp', quality:
|
|
482
|
+
* 0..1 (lossy formats, default 0.85), scale (default 2) }`. PNG is
|
|
483
|
+
* lossless; JPEG/WebP are typically a fraction of the size on a long
|
|
484
|
+
* template. Read the returned `blob.type` for what was actually encoded --
|
|
485
|
+
* a browser without a WebP encoder hands back PNG. See
|
|
486
|
+
* render/screenshot.js for the technique and its limits.
|
|
487
|
+
*/
|
|
488
|
+
screenshotPng(options) { return captureTemplatePng(this.core, this.mc, options); }
|
|
478
489
|
|
|
479
490
|
/** Opens the story-style viewer over the editor and captures into it -- what the Screenshot button does. Nothing touches the filesystem until the user picks Download inside it. */
|
|
480
491
|
previewScreenshot() { this.story.open(); }
|
|
481
492
|
|
|
482
|
-
/** `screenshotPng()` plus the same save-a-file flow as the HTML export download, with success/failure toasts. Pass an already-captured blob (the story viewer does) to save that one instead of rendering a second time. */
|
|
483
|
-
async downloadScreenshot(blob) {
|
|
493
|
+
/** `screenshotPng(options)` plus the same save-a-file flow as the HTML export download, with success/failure toasts. Pass an already-captured blob (the story viewer does) to save that one instead of rendering a second time; the filename extension follows the blob's actual type. */
|
|
494
|
+
async downloadScreenshot(blob, options) {
|
|
484
495
|
const t = this.core.t;
|
|
485
496
|
try {
|
|
486
|
-
const png = blob || await this.screenshotPng();
|
|
497
|
+
const png = blob || await this.screenshotPng(options);
|
|
498
|
+
const ext = { 'image/jpeg': 'jpg', 'image/webp': 'webp' }[png.type] || 'png';
|
|
487
499
|
const a = document.createElement('a');
|
|
488
500
|
a.href = URL.createObjectURL(png);
|
|
489
|
-
a.download = 'email.
|
|
501
|
+
a.download = 'email.' + ext;
|
|
490
502
|
a.click();
|
|
491
503
|
setTimeout(() => URL.revokeObjectURL(a.href), 1500);
|
|
492
504
|
this.core.flash(t('toast.pngSaved'));
|
package/src/render/block-body.js
CHANGED
|
@@ -158,8 +158,14 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
158
158
|
// `inline-table` so the wrapper's text-align still positions it; the
|
|
159
159
|
// `align` attribute is the same instruction for Word, which ignores the
|
|
160
160
|
// display value. A full-width button is a plain 100% table instead.
|
|
161
|
+
// `float:none` is load-bearing: browsers map `align="left|right"` on a
|
|
162
|
+
// table to a float presentational hint, which takes the pill out of
|
|
163
|
+
// flow -- the block collapses to its padding on the canvas and the
|
|
164
|
+
// button paints over the next block. The inline style outranks the
|
|
165
|
+
// hint everywhere floats work, and Word ignores CSS float, so the
|
|
166
|
+
// `align` attribute still does its one job there.
|
|
161
167
|
const table = el('table', {
|
|
162
|
-
display: p.full ? 'table' : 'inline-table', width: p.full ? '100%' : 'auto', borderCollapse: 'separate',
|
|
168
|
+
display: p.full ? 'table' : 'inline-table', width: p.full ? '100%' : 'auto', borderCollapse: 'separate', cssFloat: 'none',
|
|
163
169
|
}, { role: 'presentation', cellpadding: '0', cellspacing: '0', border: '0', align: p.full ? undefined : p.align });
|
|
164
170
|
const td = el('td', {
|
|
165
171
|
background: p.bg, borderRadius: p.radius + 'px', padding: p.py + 'px ' + p.px + 'px', textAlign: 'center',
|