@seliseblocks/mailcraft 0.1.0 → 0.2.0
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/DOCS.md +240 -12
- package/README.md +11 -3
- package/README.md.txt +134 -0
- package/dist/mailcraft-editor.bundle.js +51 -39
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/order-confirmed.html +80 -80
- package/examples/vanilla.html +242 -23
- package/package.json +7 -2
- package/src/core/assets.js +10 -15
- package/src/core/binder.js +120 -118
- package/src/core/blocks.js +14 -1
- package/src/core/css-cascade.js +117 -117
- package/src/core/editor-core.js +1530 -1492
- package/src/core/export.js +142 -55
- package/src/core/i18n/ar.js +219 -177
- package/src/core/i18n/bg.js +196 -152
- package/src/core/i18n/bn.js +218 -176
- package/src/core/i18n/ca.js +196 -152
- package/src/core/i18n/cs.js +196 -152
- package/src/core/i18n/da.js +196 -152
- package/src/core/i18n/de-CH.js +196 -152
- package/src/core/i18n/de.js +196 -152
- package/src/core/i18n/dz.js +221 -179
- package/src/core/i18n/el.js +196 -152
- package/src/core/i18n/en.js +3 -10
- package/src/core/i18n/es.js +196 -152
- package/src/core/i18n/et.js +196 -152
- package/src/core/i18n/fi.js +196 -152
- package/src/core/i18n/fr.js +196 -152
- package/src/core/i18n/hr.js +196 -152
- package/src/core/i18n/hu.js +196 -152
- package/src/core/i18n/index.js +83 -83
- package/src/core/i18n/it.js +196 -152
- package/src/core/i18n/lt.js +196 -152
- package/src/core/i18n/lv.js +196 -152
- package/src/core/i18n/nb.js +196 -152
- package/src/core/i18n/nl.js +196 -152
- package/src/core/i18n/pl.js +196 -152
- package/src/core/i18n/pt.js +196 -152
- package/src/core/i18n/ro.js +196 -152
- package/src/core/i18n/ru.js +196 -152
- package/src/core/i18n/sk.js +196 -152
- package/src/core/i18n/sl.js +196 -152
- package/src/core/i18n/sv.js +196 -152
- package/src/core/i18n/tables.js +50 -50
- package/src/core/i18n/tr.js +196 -152
- package/src/core/i18n/uk.js +196 -152
- package/src/core/icons.js +237 -235
- package/src/core/ids.js +1 -1
- package/src/core/import-html.js +1025 -959
- 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/sanitize.js +141 -141
- package/src/core/storage-limits.js +184 -184
- package/src/core/storage.js +85 -85
- package/src/core/theme.js +1 -1
- package/src/core/variables.js +11 -11
- package/src/index.js +9 -9
- package/src/mailcraft-editor.js +26 -14
- package/src/render/block-body.js +49 -6
- package/src/render/canvas.js +31 -2
- package/src/render/fields.js +602 -588
- package/src/render/focus-preserve.js +158 -158
- package/src/render/rte.js +241 -212
- package/src/render/screenshot.js +132 -132
- package/src/render/story.js +415 -415
- package/src/render/style.js +8 -0
- package/types/index.d.ts +419 -0
package/src/core/export.js
CHANGED
|
@@ -1,58 +1,145 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ported verbatim from the original `buildHtml()`, including its defining
|
|
3
|
-
* trait: it does not re-template each block from `props` a second time, it
|
|
4
|
-
* reads back the already-rendered live DOM node for each block (`grab`) and
|
|
5
|
-
* strips editor-only attributes. The one adaptation: the original queried
|
|
6
|
-
* plain `document` (the whole prototype ran outside any shadow tree); this
|
|
7
|
-
* package renders inside a Shadow DOM, so `grab` is parameterized on the root
|
|
8
|
-
* to search (the element's `shadowRoot`) instead of hardcoding `document`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { cssUrl } from './sanitize.js';
|
|
1
|
+
/**
|
|
2
|
+
* Ported verbatim from the original `buildHtml()`, including its defining
|
|
3
|
+
* trait: it does not re-template each block from `props` a second time, it
|
|
4
|
+
* reads back the already-rendered live DOM node for each block (`grab`) and
|
|
5
|
+
* strips editor-only attributes. The one adaptation: the original queried
|
|
6
|
+
* plain `document` (the whole prototype ran outside any shadow tree); this
|
|
7
|
+
* package renders inside a Shadow DOM, so `grab` is parameterized on the root
|
|
8
|
+
* to search (the element's `shadowRoot`) instead of hardcoding `document`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { cssUrl } from './sanitize.js';
|
|
12
12
|
import { rowBorderCss, rowMargin, rowPad } from './layout-style.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
13
|
+
|
|
14
|
+
const LOGIC_OPEN = { condition: '{{#if ', loop: '{{#each ' };
|
|
15
|
+
const LOGIC_CLOSE = { condition: '{{/if}}', loop: '{{/each}}' };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What each dynamic-content marker block actually emits, decided over the
|
|
19
|
+
* whole document in reading order so the output template is always balanced
|
|
20
|
+
* no matter how the markers were arranged or mangled: an end with no open of
|
|
21
|
+
* its kind on top of the stack emits nothing, and opens still unclosed after
|
|
22
|
+
* the last row are closed in `tail`, appended after the final row. Markers
|
|
23
|
+
* emit literal template tags -- like merge tags, the editor never evaluates
|
|
24
|
+
* them; the host's engine runs before any mail client parses the HTML, so raw
|
|
25
|
+
* {{#each}} text in the markup is the standard pattern, not invalid HTML.
|
|
26
|
+
* Angle brackets are stripped from expressions: no Handlebars-style
|
|
27
|
+
* expression contains them, and it keeps a typed '<' from opening a tag in
|
|
28
|
+
* the sent document.
|
|
29
|
+
*/
|
|
30
|
+
function logicPlan(doc) {
|
|
31
|
+
const expr = (v) => String(v || '').replace(/[<>]/g, '').trim();
|
|
32
|
+
const emit = new Map();
|
|
33
|
+
const stack = [];
|
|
34
|
+
doc.rows.forEach((r) => r.cols.forEach((c) => c.blocks.forEach((b) => {
|
|
35
|
+
if (b.type !== 'condition' && b.type !== 'loop') return;
|
|
36
|
+
if (!b.props.end) {
|
|
37
|
+
const e = expr(b.props.expr);
|
|
38
|
+
emit.set(b.id, e ? LOGIC_OPEN[b.type] + e + '}}' : '');
|
|
39
|
+
if (e) stack.push(b.type);
|
|
40
|
+
} else if (stack[stack.length - 1] === b.type) {
|
|
41
|
+
stack.pop();
|
|
42
|
+
emit.set(b.id, LOGIC_CLOSE[b.type]);
|
|
43
|
+
} else {
|
|
44
|
+
emit.set(b.id, '');
|
|
45
|
+
}
|
|
46
|
+
})));
|
|
47
|
+
const tail = stack.reverse().map((k) => LOGIC_CLOSE[k]).join('\n ');
|
|
48
|
+
return { emit, tail };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Display-only dressing for the Code modal's live-preview iframe: the
|
|
53
|
+
* exported template carries literal {{#if}}/{{#each}} tags, which an iframe
|
|
54
|
+
* renders as bare text at odd positions (a browser even foster-parents the
|
|
55
|
+
* between-row ones out of the table entirely). For *viewing*, each tag
|
|
56
|
+
* becomes the same dashed band the canvas draws -- tags between <tr>s become
|
|
57
|
+
* a slim full-width row so they hold their place in the table, tags inside a
|
|
58
|
+
* cell become an inline chip. Never applied to the HTML the host receives:
|
|
59
|
+
* exportHtml/copy/download all carry the real tags.
|
|
60
|
+
*/
|
|
61
|
+
export function decorateLogicTags(html) {
|
|
62
|
+
const esc = (v) => String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
63
|
+
const chip = (kind, expr, end) => {
|
|
64
|
+
const color = kind === 'each' ? '#7c3aed' : '#0e7490';
|
|
65
|
+
const word = end ? (kind === 'each' ? 'END LOOP' : 'END IF') : (kind === 'each' ? 'REPEAT EACH' : 'SHOW IF');
|
|
66
|
+
return '<span style="display:inline-flex;align-items:center;gap:7px;box-sizing:border-box;border:1.5px dashed ' + color + ';border-radius:7px;background:' + color + '14;padding:4px 10px;margin:2px 0;color:' + color + ';font-family:ui-monospace,monospace;font-size:9.5px;font-weight:700;letter-spacing:0.12em;">'
|
|
67
|
+
+ (end ? '⏶ ' : '⏷ ') + word
|
|
68
|
+
+ (expr ? ' <span style="font-weight:400;font-size:11px;letter-spacing:0;">{{ ' + esc(expr) + ' }}</span>' : '')
|
|
69
|
+
+ '</span>';
|
|
70
|
+
};
|
|
71
|
+
const bandRow = (kind, expr, end) => '<tr><td colspan="99" style="padding:2px 8px;">' + chip(kind, expr, end) + '</td></tr>';
|
|
72
|
+
let s = String(html || '');
|
|
73
|
+
// Between-row tags first (adjacent to a <tr> or after a </tr>), looped for
|
|
74
|
+
// the same adjacency reason as import-html's foldLogicWrappers.
|
|
75
|
+
const parse = (tag) => { const m = tag.match(/^#(if|each)\s+(.+)$/); return m ? [m[1], m[2].trim(), false] : [tag.slice(1), '', true]; };
|
|
76
|
+
for (let i = 0; i < 6; i++) {
|
|
77
|
+
const before = s;
|
|
78
|
+
s = s.replace(/\{\{(#(?:if|each)\s+[^{}]+?|\/(?:if|each))\s*\}\}(?=\s*(?:<tr[\s>]|<\/table))/gi, (m, tag) => bandRow(...parse(tag)));
|
|
79
|
+
s = s.replace(/(<\/tr>\s*)\{\{(#(?:if|each)\s+[^{}]+?|\/(?:if|each))\s*\}\}/gi, (m, pre, tag) => pre + bandRow(...parse(tag)));
|
|
80
|
+
if (s === before) break;
|
|
81
|
+
}
|
|
82
|
+
// Whatever remains sits inside a cell (or prose): inline chips in place.
|
|
83
|
+
s = s.replace(/\{\{#(if|each)\s+([^{}]+?)\s*\}\}/gi, (m, kind, expr) => chip(kind.toLowerCase(), expr, false));
|
|
84
|
+
s = s.replace(/\{\{\/(if|each)\s*\}\}/gi, (m, kind) => chip(kind.toLowerCase(), '', true));
|
|
85
|
+
return s;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildHtml(state, root, boxCss) {
|
|
89
|
+
const d = state.doc; const t = d.theme;
|
|
90
|
+
const logic = logicPlan(d);
|
|
91
|
+
const grab = (id) => {
|
|
92
|
+
const el = root.querySelector('[data-mc-content="' + id + '"]');
|
|
93
|
+
if (!el) return '';
|
|
94
|
+
return el.outerHTML
|
|
95
|
+
.replace(/\scontenteditable="[^"]*"/g, '')
|
|
96
|
+
.replace(/\sdata-mc-[a-z-]+="[^"]*"/g, '')
|
|
97
|
+
.replace(/\sspellcheck="[^"]*"/g, '')
|
|
98
|
+
.replace(/\sdata-(?:gramm|gramm_editor|enable-grammarly|lt-active)="[^"]*"/g, '')
|
|
99
|
+
.replace(/\sdraggable="[^"]*"/g, '');
|
|
100
|
+
};
|
|
101
|
+
const rows = d.rows.map((r) => {
|
|
102
|
+
const rp = r.props;
|
|
103
|
+
// A row holding nothing but logic markers exists to wrap the *sections*
|
|
104
|
+
// around it (drop a Condition onto the canvas above and below a group of
|
|
105
|
+
// rows). Emitting its <tr> scaffolding would leave an empty padded band
|
|
106
|
+
// in the sent email, so only the tags themselves are emitted -- they sit
|
|
107
|
+
// between <tr>s as plain text, which the host's engine consumes before
|
|
108
|
+
// any mail client parses the markup.
|
|
109
|
+
const blocksOf = r.cols.reduce((a, c) => a.concat(c.blocks), []);
|
|
110
|
+
if (blocksOf.length && blocksOf.every((b) => b.type === 'condition' || b.type === 'loop')) {
|
|
111
|
+
return blocksOf.map((b) => logic.emit.get(b.id) || '').filter(Boolean).map((s) => ' ' + s).join('\n');
|
|
112
|
+
}
|
|
113
|
+
const colInner = (c) => c.blocks.map((b) => {
|
|
114
|
+
if (b.type === 'css') return '<style>' + (b.props.code || '') + '</style>';
|
|
115
|
+
if (b.type === 'html') return b.props.code || '';
|
|
116
|
+
if (b.type === 'svg') return '<div style="text-align:' + b.props.align + ';padding:' + b.props.py + 'px 0">' + (b.props.code || '') + '</div>';
|
|
117
|
+
if (b.type === 'condition' || b.type === 'loop') return logic.emit.get(b.id) || '';
|
|
118
|
+
return '<div style="' + boxCss(b.props) + '">' + grab(b.id) + '</div>';
|
|
119
|
+
}).filter(Boolean).join('\n ') || ' ';
|
|
120
|
+
const cells = r.cols.map((c) => {
|
|
121
|
+
// Column-level styling (bg/radius/inner padding) renders as a wrapper
|
|
122
|
+
// <div> inside the cell so the gutter padding stays unpainted --
|
|
123
|
+
// mirrors render/canvas.js's `host` wrapper.
|
|
124
|
+
const inner = (c.bg || c.border || c.radius || c.padY || c.padX)
|
|
125
|
+
? '<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>'
|
|
126
|
+
: colInner(c);
|
|
127
|
+
return '<td width="' + c.span + '%" valign="' + rp.valign + '" style="padding:0 ' + Math.round(rp.gap / 2) + 'px;">\n ' + inner + '\n </td>';
|
|
128
|
+
}).join('\n ');
|
|
129
|
+
const cssBody = rp.layout === 'grid'
|
|
130
|
+
? '<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>'
|
|
131
|
+
: '<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>';
|
|
132
|
+
const body = rp.layout && rp.layout !== 'columns'
|
|
133
|
+
? cssBody
|
|
134
|
+
: '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>\n ' + cells + '\n </tr></table>';
|
|
135
|
+
const tdBg = rp.bgImage
|
|
136
|
+
? 'background-color:' + (rp.bg || t.contentBg) + ';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') + ';'
|
|
137
|
+
: 'background:' + (rp.bg || t.contentBg) + ';';
|
|
138
|
+
const tdBorder = rowBorderCss(rp)
|
|
139
|
+
+ (rp.radius ? 'border-radius:' + rp.radius + 'px;' : '')
|
|
53
140
|
+ (rp.shadow ? 'box-shadow:' + rp.shadow + ';' : '')
|
|
54
141
|
+ ((rp.mt || rp.mr || rp.mb || rp.ml || rp.my) ? 'margin:' + rowMargin(rp) + ';' : '');
|
|
55
|
-
return ' <tr>\n <td style="padding:' + rowPad(rp) + ';' + tdBg + tdBorder + '">\n ' + body + '\n </td>\n </tr>';
|
|
56
|
-
}).join('\n');
|
|
57
|
-
return '<!doctype html>\n<html lang="en">\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 dark">\n<title>' + 'Email' + '</title>\n</head>\n<body style="margin:0;padding:0;background:' + t.bg + ';font-family:' + t.font.replace(/"/g, "'") + ';color:' + t.text + ';-webkit-font-smoothing:antialiased;">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:' + t.bg + ';">\n <tr><td align="center" style="padding:24px 12px;">\n <table role="presentation" width="' + t.width + '" cellpadding="0" cellspacing="0" border="0" style="width:' + t.width + 'px;max-width:100%;background:' + t.contentBg + ';">\n' + rows + '\n </table>\n </td></tr>\n</table>\n</body>\n</html>';
|
|
58
|
-
}
|
|
142
|
+
return ' <tr>\n <td style="padding:' + rowPad(rp) + ';' + tdBg + tdBorder + '">\n ' + body + '\n </td>\n </tr>';
|
|
143
|
+
}).join('\n') + (logic.tail ? '\n ' + logic.tail : '');
|
|
144
|
+
return '<!doctype html>\n<html lang="en">\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 dark">\n<title>' + 'Email' + '</title>\n</head>\n<body style="margin:0;padding:0;background:' + t.bg + ';font-family:' + t.font.replace(/"/g, "'") + ';color:' + t.text + ';-webkit-font-smoothing:antialiased;">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:' + t.bg + ';">\n <tr><td align="center" style="padding:24px 12px;">\n <table role="presentation" width="' + t.width + '" cellpadding="0" cellspacing="0" border="0" style="width:' + t.width + 'px;max-width:100%;background:' + t.contentBg + ';">\n' + rows + '\n </table>\n </td></tr>\n</table>\n</body>\n</html>';
|
|
145
|
+
}
|