@seliseblocks/mailcraft 0.2.5 → 0.2.6
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 +36 -32
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/package.json +1 -1
- package/src/core/editor-core.js +6 -0
- package/src/core/export.js +8 -1
- package/src/core/import-html.js +63 -9
- package/src/core/theme.js +1 -1
- package/src/render/canvas.js +2 -0
- package/src/render/style.js +4 -0
package/package.json
CHANGED
package/src/core/editor-core.js
CHANGED
|
@@ -1501,6 +1501,12 @@ export class EditorCore {
|
|
|
1501
1501
|
B.color('Content area background color', 'contentBg', { transparent: true, solid: '#ffffff' }),
|
|
1502
1502
|
B.range('Corner radius', 'radius', 0, 48, 1, 'px'),
|
|
1503
1503
|
B.range('Border thickness', 'borderW', 0, 12, 1, 'px'),
|
|
1504
|
+
B.sel('Drop shadow', 'shadow', [
|
|
1505
|
+
{ value: '', label: 'None' },
|
|
1506
|
+
{ value: '0 2px 8px rgba(23,32,51,0.08)', label: 'Soft' },
|
|
1507
|
+
{ value: '0 8px 28px rgba(23,32,51,0.14)', label: 'Medium' },
|
|
1508
|
+
{ value: '0 18px 48px rgba(23,32,51,0.22)', label: 'Deep' },
|
|
1509
|
+
]),
|
|
1504
1510
|
].concat(this.state.doc.theme.borderW ? [
|
|
1505
1511
|
B.sel('Border style', 'borderStyle', BORDER_STYLES),
|
|
1506
1512
|
B.color('Border color', 'borderColor'),
|
package/src/core/export.js
CHANGED
|
@@ -93,6 +93,10 @@ export function buildHtml(state, root, boxCss) {
|
|
|
93
93
|
if (!el) return '';
|
|
94
94
|
return el.outerHTML
|
|
95
95
|
.replace(/\scontenteditable="[^"]*"/g, '')
|
|
96
|
+
// Editor bookkeeping (caret restoration): a fresh random id every
|
|
97
|
+
// import, so leaving it in shipped mail also made export -> import ->
|
|
98
|
+
// export never converge byte-for-byte.
|
|
99
|
+
.replace(/\sdata-focus-key="[^"]*"/g, '')
|
|
96
100
|
.replace(/\sdata-mc-[a-z-]+="[^"]*"/g, '')
|
|
97
101
|
.replace(/\sspellcheck="[^"]*"/g, '')
|
|
98
102
|
.replace(/\sdata-(?:gramm|gramm_editor|enable-grammarly|lt-active)="[^"]*"/g, '')
|
|
@@ -157,6 +161,9 @@ export function buildHtml(state, root, boxCss) {
|
|
|
157
161
|
// `overflow:hidden` is what actually clips a row's own background to the
|
|
158
162
|
// rounded corner; without it the first and last rows paint square over it.
|
|
159
163
|
const contentShape = (t.radius ? 'border-radius:' + t.radius + 'px;overflow:hidden;' : '')
|
|
160
|
-
+ (t.borderW ? 'border:' + t.borderW + 'px ' + (t.borderStyle || 'solid') + ' ' + (t.borderColor || '#e2e2e5') + ';' : '')
|
|
164
|
+
+ (t.borderW ? 'border:' + t.borderW + 'px ' + (t.borderStyle || 'solid') + ' ' + (t.borderColor || '#e2e2e5') + ';' : '')
|
|
165
|
+
// Patchy in mail clients (Outlook drops it), but honest: what the user
|
|
166
|
+
// styled ships, and capable clients render it.
|
|
167
|
+
+ (t.shadow ? 'box-shadow:' + t.shadow + ';' : '');
|
|
161
168
|
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:' + pageBg + ';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:' + pageBg + ';">\n <tr><td align="center" style="padding:' + pagePad + ';">\n <table role="presentation" width="' + t.width + '" cellpadding="0" cellspacing="0" border="0" style="width:' + t.width + 'px;max-width:100%;background:' + (t.contentBg || 'transparent') + ';' + contentShape + '">\n' + rows + '\n </table>\n </td></tr>\n</table>\n</body>\n</html>';
|
|
162
169
|
}
|
package/src/core/import-html.js
CHANGED
|
@@ -442,6 +442,21 @@ function logicMarkersOf(text) {
|
|
|
442
442
|
return out;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/** Whether an element (or the transparent single-child chain under it) carries its own padding -- the shape the exporter writes for a block's py/px, and a builder section's own spacing. Such a wrapper is one block, never part of a text run. */
|
|
446
|
+
function hasOwnRunPad(el) {
|
|
447
|
+
let e = el;
|
|
448
|
+
if (e.style && paddingOf(e.style)) return true;
|
|
449
|
+
while (
|
|
450
|
+
e.children && e.children.length === 1
|
|
451
|
+
&& !INLINE_TAGS.test(e.firstElementChild.tagName)
|
|
452
|
+
&& (e.textContent || '') === (e.firstElementChild.textContent || '')
|
|
453
|
+
) {
|
|
454
|
+
e = e.firstElementChild;
|
|
455
|
+
if (e.style && paddingOf(e.style)) return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
445
460
|
function blocksFromNodes(nodes) {
|
|
446
461
|
const out = [];
|
|
447
462
|
let buf = [];
|
|
@@ -458,6 +473,11 @@ function blocksFromNodes(nodes) {
|
|
|
458
473
|
// first-element-only read never saw. Per-element differences inside
|
|
459
474
|
// the run survive as the inline styles `cleanImportHtml` now keeps.
|
|
460
475
|
let baseEl = INLINE_TAGS.test(bufFirstEl.tagName) ? (bufFirstEl.parentElement || bufFirstEl) : bufFirstEl;
|
|
476
|
+
// The run's own spacing: the exporter writes a text block's py/px as
|
|
477
|
+
// padding on the wrapper div the descent below walks straight past
|
|
478
|
+
// (it lands on the <p>), so a padded block came back at the default
|
|
479
|
+
// 10px/0 on every save. First padding on the wrapper chain wins.
|
|
480
|
+
let runPad = paddingOf(baseEl.style);
|
|
461
481
|
// Then descend through transparent single-child wrappers: builders
|
|
462
482
|
// nest a `font-family:sans-serif` shim div around the div that
|
|
463
483
|
// carries the real typography, and `inheritedStyle` below walks *up*
|
|
@@ -467,7 +487,8 @@ function blocksFromNodes(nodes) {
|
|
|
467
487
|
baseEl.children.length === 1
|
|
468
488
|
&& !INLINE_TAGS.test(baseEl.firstElementChild.tagName)
|
|
469
489
|
&& (baseEl.textContent || '') === (baseEl.firstElementChild.textContent || '')
|
|
470
|
-
) baseEl = baseEl.firstElementChild;
|
|
490
|
+
) { baseEl = baseEl.firstElementChild; if (!runPad) runPad = paddingOf(baseEl.style); }
|
|
491
|
+
if (runPad) { over.py = runPad.py; over.px = runPad.px; }
|
|
471
492
|
const size = fontPx(inheritedStyle(baseEl, 'fontSize')); if (size) over.size = size;
|
|
472
493
|
const color = inheritedStyle(baseEl, 'color'); if (color) over.color = color;
|
|
473
494
|
over.align = textAlignOf(baseEl);
|
|
@@ -513,8 +534,14 @@ function blocksFromNodes(nodes) {
|
|
|
513
534
|
return;
|
|
514
535
|
}
|
|
515
536
|
if (isStructural(target)) { flush(); out.push(blk('html', { code: n.outerHTML })); return; }
|
|
537
|
+
// A wrapper carrying its own padding is a block boundary, not part of a
|
|
538
|
+
// run: the exporter writes every text block as exactly such a padded div,
|
|
539
|
+
// and buffering two of them together merged neighbouring blocks into one
|
|
540
|
+
// -- the second lost its padding, size, everything -- on every save.
|
|
541
|
+
if (n.nodeType === 1 && !INLINE_TAGS.test(target.tagName) && hasOwnRunPad(target) && buf.length) flush();
|
|
516
542
|
if (!bufFirstEl) bufFirstEl = target;
|
|
517
543
|
buf.push(n.outerHTML);
|
|
544
|
+
if (n.nodeType === 1 && !INLINE_TAGS.test(target.tagName) && hasOwnRunPad(target)) flush();
|
|
518
545
|
});
|
|
519
546
|
flush();
|
|
520
547
|
return out;
|
|
@@ -570,16 +597,26 @@ function isPassthroughTable(tb) {
|
|
|
570
597
|
return cells.length === 1;
|
|
571
598
|
}
|
|
572
599
|
|
|
600
|
+
/** CSSOM hands colors back as `rgb(r, g, b)` even when the source (and the
|
|
601
|
+
* user's own value) was hex -- so every save visibly rewrote colour fields.
|
|
602
|
+
* Solid rgb() flattens back to hex; anything else (rgba, keywords,
|
|
603
|
+
* `transparent`) passes through untouched. */
|
|
604
|
+
function hexOf(value) {
|
|
605
|
+
const m = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(String(value == null ? '' : value).trim());
|
|
606
|
+
if (!m) return value;
|
|
607
|
+
return '#' + [m[1], m[2], m[3]].map((n) => Number(n).toString(16).padStart(2, '0')).join('');
|
|
608
|
+
}
|
|
609
|
+
|
|
573
610
|
function bgOf(el) {
|
|
574
611
|
const st = el.style;
|
|
575
|
-
if (st && st.backgroundColor) return st.backgroundColor;
|
|
612
|
+
if (st && st.backgroundColor) return hexOf(st.backgroundColor);
|
|
576
613
|
// The `background` shorthand is only a usable *color* when it holds no
|
|
577
614
|
// image/gradient -- browsers populate `backgroundColor` from a shorthand
|
|
578
615
|
// that includes a color, so reaching here with a url() means there is no
|
|
579
616
|
// color to take (and taking the raw string set broken values).
|
|
580
617
|
const short = (st && st.background) || '';
|
|
581
|
-
if (short && short.indexOf('url(') < 0 && short.indexOf('gradient(') < 0) return short;
|
|
582
|
-
return (el.getAttribute && el.getAttribute('bgcolor')) || '';
|
|
618
|
+
if (short && short.indexOf('url(') < 0 && short.indexOf('gradient(') < 0) return hexOf(short);
|
|
619
|
+
return hexOf((el.getAttribute && el.getAttribute('bgcolor')) || '');
|
|
583
620
|
}
|
|
584
621
|
|
|
585
622
|
function padOf(el) {
|
|
@@ -604,7 +641,7 @@ function applyPad(rows, pad) {
|
|
|
604
641
|
|
|
605
642
|
/** Longhand side colors are always a single value; the `borderColor` shorthand reads back as a per-side list when the sides differ (`rgb(...) rgb(...) currentcolor`), which is not a usable color. */
|
|
606
643
|
function borderColorOf(st) {
|
|
607
|
-
const ok = (v) => (v && v !== 'currentcolor' ? v : '');
|
|
644
|
+
const ok = (v) => (v && v !== 'currentcolor' ? hexOf(v) : '');
|
|
608
645
|
return ok(st.borderTopColor) || ok(st.borderLeftColor) || ok(st.borderRightColor) || ok(st.borderBottomColor) || '';
|
|
609
646
|
}
|
|
610
647
|
|
|
@@ -940,7 +977,7 @@ function collectRows(nodes) {
|
|
|
940
977
|
function themeFromParsedDoc(doc) {
|
|
941
978
|
const theme = {};
|
|
942
979
|
const body = doc.body;
|
|
943
|
-
let bg = (body.style && (body.style.backgroundColor || body.style.background)) || body.getAttribute('bgcolor') || '';
|
|
980
|
+
let bg = hexOf((body.style && (body.style.backgroundColor || body.style.background)) || body.getAttribute('bgcolor') || '');
|
|
944
981
|
if (!bg) {
|
|
945
982
|
const outer = body.querySelector('table');
|
|
946
983
|
if (outer) bg = bgOf(outer) || (outer.querySelector('td') ? bgOf(outer.querySelector('td')) : '');
|
|
@@ -986,12 +1023,26 @@ function themeFromParsedDoc(doc) {
|
|
|
986
1023
|
theme.borderStyle = borderStyleOf(content.style);
|
|
987
1024
|
theme.borderColor = borderColorOf(content.style) || '';
|
|
988
1025
|
}
|
|
1026
|
+
const sh = content.style && content.style.boxShadow;
|
|
1027
|
+
if (sh && sh !== 'none') theme.shadow = sh;
|
|
1028
|
+
// CLAIMED MEANS CONSUMED. These styles now live on the theme; leaving
|
|
1029
|
+
// them on the node let the row walker's card-folding (applyFrame /
|
|
1030
|
+
// applyPad) absorb the very same border, radius, shadow and page
|
|
1031
|
+
// padding into the rows -- so one save doubled every frame the user
|
|
1032
|
+
// had set at canvas level. htmlToDoc runs this pass before the row
|
|
1033
|
+
// pass for exactly this reason.
|
|
1034
|
+
if (content.style) {
|
|
1035
|
+
content.style.border = '';
|
|
1036
|
+
content.style.borderRadius = '';
|
|
1037
|
+
content.style.boxShadow = '';
|
|
1038
|
+
}
|
|
989
1039
|
const cell = content.closest ? content.closest('td') : null;
|
|
990
1040
|
if (cell && cell.style) {
|
|
991
1041
|
const padY = PX(cell.style.paddingTop);
|
|
992
1042
|
const padX = PX(cell.style.paddingLeft);
|
|
993
1043
|
if (padY > 0) theme.padY = padY;
|
|
994
1044
|
if (padX > 0) theme.padX = padX;
|
|
1045
|
+
cell.style.padding = '';
|
|
995
1046
|
}
|
|
996
1047
|
}
|
|
997
1048
|
}
|
|
@@ -1018,8 +1069,8 @@ function themeFromParsedDoc(doc) {
|
|
|
1018
1069
|
linkCounts[c] = (linkCounts[c] || 0) + 1;
|
|
1019
1070
|
});
|
|
1020
1071
|
const link = Object.keys(linkCounts).sort((a, b) => linkCounts[b] - linkCounts[a])[0];
|
|
1021
|
-
if (link) theme.link = link;
|
|
1022
|
-
if (body.style && body.style.color) theme.text = body.style.color;
|
|
1072
|
+
if (link) theme.link = hexOf(link);
|
|
1073
|
+
if (body.style && body.style.color) theme.text = hexOf(body.style.color);
|
|
1023
1074
|
return theme;
|
|
1024
1075
|
}
|
|
1025
1076
|
|
|
@@ -1057,7 +1108,10 @@ export function htmlToDoc(src) {
|
|
|
1057
1108
|
// (never-inlined exports, hand-written emails) classify like inlined ones.
|
|
1058
1109
|
// Best-effort: a pathological stylesheet must never block the import.
|
|
1059
1110
|
try { inlineStylesheets(doc); } catch { /* proceed with inline styles only */ }
|
|
1060
|
-
|
|
1111
|
+
// Theme first: themeFromParsedDoc consumes the styles it claims off the
|
|
1112
|
+
// scaffold nodes, and the row walker must see the cleaned DOM.
|
|
1113
|
+
const theme = themeFromParsedDoc(doc);
|
|
1114
|
+
return { rows: collectRows(Array.from(doc.body.childNodes)), theme };
|
|
1061
1115
|
}
|
|
1062
1116
|
|
|
1063
1117
|
export function htmlToRows(src) {
|
package/src/core/theme.js
CHANGED
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
* accept the literal `transparent` (and any rgba()/#rrggbbaa value) as well
|
|
13
13
|
* as a hex colour.
|
|
14
14
|
*/
|
|
15
|
-
export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, padY: 0, padX: 0, radius: 0, borderW: 0, borderStyle: 'solid', borderColor: '#e2e2e5', font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
|
|
15
|
+
export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, padY: 0, padX: 0, radius: 0, borderW: 0, borderStyle: 'solid', borderColor: '#e2e2e5', shadow: '', font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
|
package/src/render/canvas.js
CHANGED
|
@@ -186,6 +186,8 @@ export function renderDoc(core, live) {
|
|
|
186
186
|
// Same contract for the full content-area border: set only when asked,
|
|
187
187
|
// so the chrome's own subtle frame keeps marking the sheet otherwise.
|
|
188
188
|
border: borderW ? borderW + 'px ' + (theme.borderStyle || 'solid') + ' ' + (theme.borderColor || '#e2e2e5') : '',
|
|
189
|
+
// And for the canvas-wide drop shadow -- unset keeps the chrome's own.
|
|
190
|
+
boxShadow: theme.shadow || '',
|
|
189
191
|
// Clipping the rows to that corner is right for the sent email (export
|
|
190
192
|
// emits `overflow:hidden` alongside the radius) and for the static
|
|
191
193
|
// preview, but not for the editable canvas: the sheet is also what the
|
package/src/render/style.js
CHANGED
|
@@ -304,6 +304,10 @@ export const STYLE = `
|
|
|
304
304
|
#mc .mc-inspector .mc-section-body { padding: 12px 14px 16px !important; }
|
|
305
305
|
#mc .mc-inspector .mc-section-body > :first-child { margin-top: 0 !important; }
|
|
306
306
|
#mc .mc-field-list { display: grid; align-content: start; gap: 12px; }
|
|
307
|
+
/* The author display:grid above beats the UA rule [hidden]{display:none},
|
|
308
|
+
which is exactly how the accordion silently stopped collapsing: the bar
|
|
309
|
+
toggled the hidden attribute and the grid stayed painted. Hidden wins. */
|
|
310
|
+
#mc .mc-field-list[hidden], #mc .mc-section-body[hidden] { display: none !important; }
|
|
307
311
|
#mc .mc-field-list .mc-field { min-width: 0; }
|
|
308
312
|
#mc .mc-field-list .mc-field-row {
|
|
309
313
|
display: grid !important; grid-template-columns: minmax(0, 1fr) 168px;
|