@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/mailcraft-editor.js
CHANGED
|
@@ -8,6 +8,7 @@ import { renderField, renderFieldCards, typeCommit } from './render/fields.js';
|
|
|
8
8
|
import { icon } from './core/icons.js';
|
|
9
9
|
import { TOKEN } from './core/variables.js';
|
|
10
10
|
import { hl, cssUrl } from './core/sanitize.js';
|
|
11
|
+
import { decorateLogicTags } from './core/export.js';
|
|
11
12
|
import { withFocusPreserved } from './render/focus-preserve.js';
|
|
12
13
|
import { captureTemplatePng } from './render/screenshot.js';
|
|
13
14
|
import { resolveToolbar, toolbarKey } from './core/toolbar.js';
|
|
@@ -310,7 +311,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
310
311
|
this.core.emit();
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
/** `dir` wins when the host sets it explicitly; otherwise it defaults from `isRtl(locale)` so `locale="ar"` gets RTL for free. Applied directly to `#mc` (not full state, since it
|
|
314
|
+
/** `dir` wins when the host sets it explicitly; otherwise it defaults from `isRtl(locale)` so `locale="ar"` gets RTL for free. Applied directly to `#mc` (not full state, since it only mirrors the editor chrome). The email sheet, the code editor, and the value-shaped inspector fields pin their own `dir` so the document being built never flips with the chrome (see render/canvas.js renderDoc). */
|
|
314
315
|
applyDir() {
|
|
315
316
|
if (!this.mc) return;
|
|
316
317
|
const explicit = this.getAttribute('dir');
|
|
@@ -417,10 +418,11 @@ export class MailCraftEditor extends ElementBase {
|
|
|
417
418
|
|
|
418
419
|
/**
|
|
419
420
|
* Host-supplied file storage -- see `core/storage.js` for the contract, and
|
|
420
|
-
* README.md for a worked example.
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
421
|
+
* README.md for a worked example. The package ships no files of its own: with
|
|
422
|
+
* nothing set here the library opens empty and holds only what is dropped
|
|
423
|
+
* into it, in this browser, in this draft. Setting a provider hands the whole
|
|
424
|
+
* library to the backend -- folders, paging, upload and delete all go through
|
|
425
|
+
* it, and nothing is written to localStorage. Set `null` to go back.
|
|
424
426
|
*/
|
|
425
427
|
get storageProvider() { return this.core.storageProvider; }
|
|
426
428
|
set storageProvider(provider) { this.core.setStorageProvider(provider); }
|
|
@@ -1266,7 +1268,8 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1266
1268
|
line.addEventListener('click', () => this.core.select('block', b.id));
|
|
1267
1269
|
const chip = elS('span', `width: 22px; height: 22px; flex: none; border-radius: 6px; display: flex; align-items: center; justify-content: center; background: ${bSel ? 'var(--ed-accent)' : 'var(--ed-soft)'}; color: ${bSel ? 'var(--ed-accent-ink)' : 'var(--ed-accent)'};`);
|
|
1268
1270
|
chip.appendChild(icon(b.type, 12));
|
|
1269
|
-
|
|
1271
|
+
// dir=auto: this excerpt is the email's own copy, not chrome text.
|
|
1272
|
+
line.append(chip, elS('span', 'font-family: var(--ed-font); font-size: 12px; font-weight: 500; color: var(--ed-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', { text: text || typeName, class: 'mc-tree-title', dir: 'auto' }));
|
|
1270
1273
|
if (text) {
|
|
1271
1274
|
line.append(
|
|
1272
1275
|
elS('span', 'flex: 1; min-width: 8px;'),
|
|
@@ -1305,7 +1308,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1305
1308
|
wrap.appendChild(this.sectionBar(t('vars.title'), t('vars.fromCode', { count: this.core.vars().length })));
|
|
1306
1309
|
const card = this.sectionBody();
|
|
1307
1310
|
card.appendChild(elS('p', 'margin: 0 0 12px; font-size: 11.5px; color: var(--ed-muted); line-height: 1.55;', { text: t('vars.declaredNote') }));
|
|
1308
|
-
const search = elS('input', 'width: 100%; box-sizing: border-box; background: var(--ed-panel-2); border: 1px solid var(--ed-line); border-radius: 8px; color: var(--ed-text); font: inherit; font-size: 12.5px; padding: 7px 9px; margin-bottom: 8px;', { placeholder: t('vars.filterPlaceholder'), 'data-focus-key': 'var-query' });
|
|
1311
|
+
const search = elS('input', 'width: 100%; box-sizing: border-box; background: var(--ed-panel-2); border: 1px solid var(--ed-line); border-radius: 8px; color: var(--ed-text); font: inherit; font-size: 12.5px; padding: 7px 9px; margin-bottom: 8px;', { placeholder: t('vars.filterPlaceholder'), 'data-focus-key': 'var-query', dir: 'auto' });
|
|
1309
1312
|
search.value = s.varQuery || '';
|
|
1310
1313
|
search.addEventListener('focus', () => { search.style.borderColor = 'var(--ed-accent)'; search.style.outline = 'none'; });
|
|
1311
1314
|
search.addEventListener('blur', () => { search.style.borderColor = 'var(--ed-line)'; });
|
|
@@ -1326,7 +1329,8 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1326
1329
|
const textSpan = elS('span', 'flex: 1; min-width: 0;');
|
|
1327
1330
|
textSpan.append(
|
|
1328
1331
|
elS('span', 'display: block; font-family: var(--ed-font); font-weight: 600; font-size: 12.5px; line-height: 1.2;', { text: name }),
|
|
1329
|
-
|
|
1332
|
+
// dir=ltr keeps the braces on the right ends of {{ token }} under RTL.
|
|
1333
|
+
elS('span', 'display: block; font-family: ui-monospace, monospace; font-size: 10px; color: var(--ed-accent); margin-top: 2px;', { text: TOKEN(v), dir: 'ltr' }),
|
|
1330
1334
|
);
|
|
1331
1335
|
btn.append(textSpan, elS('span', 'font-family: var(--ed-font); font-size: 9.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ed-faint);', { text: t('vars.insert') }));
|
|
1332
1336
|
grid.appendChild(btn);
|
|
@@ -1356,7 +1360,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1356
1360
|
elS('div', 'font-family: var(--ed-font); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ed-muted);', { text: t('modal.fileManager'), 'data-i18n': 'modal.fileManager' }),
|
|
1357
1361
|
(this.libraryTitleText = elS('div', 'font-family: var(--ed-font); font-weight: 600; font-size: 16px; line-height: 1.25;')),
|
|
1358
1362
|
);
|
|
1359
|
-
this.assetSearch = elS('input', 'width: 210px; box-sizing: border-box; background: var(--ed-panel-2); border: 1px solid var(--ed-line); color: var(--ed-text); font: inherit; font-size: 13px; padding: 7px 9px;', { placeholder: t('library.searchPlaceholder'), 'data-i18n-placeholder': 'library.searchPlaceholder', 'data-focus-key': 'asset-query' });
|
|
1363
|
+
this.assetSearch = elS('input', 'width: 210px; box-sizing: border-box; background: var(--ed-panel-2); border: 1px solid var(--ed-line); color: var(--ed-text); font: inherit; font-size: 13px; padding: 7px 9px;', { placeholder: t('library.searchPlaceholder'), 'data-i18n-placeholder': 'library.searchPlaceholder', 'data-focus-key': 'asset-query', dir: 'auto' });
|
|
1360
1364
|
this.assetSearch.addEventListener('focus', () => { this.assetSearch.style.borderColor = 'var(--ed-accent)'; this.assetSearch.style.outline = 'none'; });
|
|
1361
1365
|
this.assetSearch.addEventListener('blur', () => { this.assetSearch.style.borderColor = 'var(--ed-line)'; });
|
|
1362
1366
|
// Debounced re-render; setAssetQuery separately debounces the backend call.
|
|
@@ -1516,7 +1520,9 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1516
1520
|
head.append(headText, this.copyBtn, this.shotBtn, downloadBtn, closeBtn);
|
|
1517
1521
|
modal.appendChild(head);
|
|
1518
1522
|
|
|
1519
|
-
|
|
1523
|
+
// dir=ltr: this holds HTML source, which bidi-scrambles under the RTL
|
|
1524
|
+
// chrome (same reason as buildCodeEditor).
|
|
1525
|
+
this.exportTextarea = elS('textarea', 'width: 100%; height: 100%; box-sizing: border-box; border: 0; background: var(--ed-work); color: var(--ed-text); font-family: ui-monospace, monospace; font-size: 11.5px; line-height: 1.55; padding: 18px 20px; resize: none;', { readonly: 'true', dir: 'ltr' });
|
|
1520
1526
|
modal.appendChild(this.exportTextarea);
|
|
1521
1527
|
this.exportMetaEl = elS('div', 'padding: 10px 20px; border-top: 1px solid var(--ed-line); font-family: ui-monospace, monospace; font-size: 9.5px; letter-spacing: 0.06em; color: var(--ed-faint);');
|
|
1522
1528
|
modal.appendChild(this.exportMetaEl);
|
|
@@ -1601,7 +1607,10 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1601
1607
|
* highlight or caret drifting away from the editable text. */
|
|
1602
1608
|
buildCodeEditor() {
|
|
1603
1609
|
const MONO = 'font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 20px; tab-size: 2;';
|
|
1604
|
-
|
|
1610
|
+
// dir=ltr: HTML source is directional text; under an RTL chrome the
|
|
1611
|
+
// inherited direction would bidi-scramble it and, worse, let the caret
|
|
1612
|
+
// in the transparent textarea drift off the highlighted layer beneath.
|
|
1613
|
+
const root = elS('div', 'position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden; background: var(--ed-work);', { dir: 'ltr' });
|
|
1605
1614
|
const inner = elS('div', 'position: relative; min-height: 100%; width: 100%; min-width: 0;');
|
|
1606
1615
|
this.codePre = elS('pre', `${MONO} margin: 0; padding: 14px 0 80px; width: 100%; min-width: 0; min-height: 100%; white-space: normal; color: var(--ed-text); pointer-events: none; background: linear-gradient(to right, var(--ed-panel-2) 0 52px, var(--ed-work) 52px);`, { 'aria-hidden': 'true' });
|
|
1607
1616
|
this.codeTextarea = elS('textarea', `${MONO} position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 14px 18px 80px 70px; border: 0; background: transparent; color: transparent; caret-color: var(--ed-accent); white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; overflow: hidden; resize: none; outline: none;`, { spellcheck: 'false', wrap: 'soft', 'data-focus-key': 'code-src' });
|
|
@@ -1645,7 +1654,10 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1645
1654
|
if (!s.codeOpen) return;
|
|
1646
1655
|
this.refreshCodeSource();
|
|
1647
1656
|
if (this._codeFrameSrc !== s.codeLive) {
|
|
1648
|
-
|
|
1657
|
+
// Display-only: the preview pane draws logic tags as the canvas's
|
|
1658
|
+
// dashed bands; the source pane and everything exported keep the
|
|
1659
|
+
// literal {{#if}}/{{#each}} text.
|
|
1660
|
+
this.codeFrame.srcdoc = decorateLogicTags(s.codeLive);
|
|
1649
1661
|
this._codeFrameSrc = s.codeLive;
|
|
1650
1662
|
}
|
|
1651
1663
|
this.syncCodePreviewScrollbar();
|
|
@@ -1730,7 +1742,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1730
1742
|
|
|
1731
1743
|
const briefWrap = elS('div');
|
|
1732
1744
|
briefWrap.appendChild(elS('label', 'display: block; font-size: 11.5px; color: var(--ed-muted); margin-bottom: 4px;', { text: t('ai.briefLabel'), 'data-i18n': 'ai.briefLabel' }));
|
|
1733
|
-
this.aiBriefInput = elS('textarea', 'width: 100%; min-height: 78px; box-sizing: border-box; background: var(--ed-panel-2); border: 1px solid var(--ed-line); color: var(--ed-text); font: inherit; font-size: 13px; padding: 9px; resize: vertical;', { placeholder: t('ai.briefPlaceholder'), 'data-i18n-placeholder': 'ai.briefPlaceholder', 'data-focus-key': 'ai-brief' });
|
|
1745
|
+
this.aiBriefInput = elS('textarea', 'width: 100%; min-height: 78px; box-sizing: border-box; background: var(--ed-panel-2); border: 1px solid var(--ed-line); color: var(--ed-text); font: inherit; font-size: 13px; padding: 9px; resize: vertical;', { placeholder: t('ai.briefPlaceholder'), 'data-i18n-placeholder': 'ai.briefPlaceholder', 'data-focus-key': 'ai-brief', dir: 'auto' });
|
|
1734
1746
|
this.aiBriefInput.addEventListener('focus', () => { this.aiBriefInput.style.borderColor = 'var(--ed-accent)'; this.aiBriefInput.style.outline = 'none'; });
|
|
1735
1747
|
// Debounced with a blur flush: runAi reads state.aiBrief, and clicking
|
|
1736
1748
|
// Generate blurs this field first -- the flush lands the last keystrokes
|
|
@@ -1772,7 +1784,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1772
1784
|
s.aiResults.forEach((r) => {
|
|
1773
1785
|
const card = elS('div', 'position: relative; border: 1px solid var(--ed-line); border-radius: var(--ed-radius-sm); padding: 15px;');
|
|
1774
1786
|
card.appendChild(elS('div', 'font-family: var(--ed-font); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ed-accent); margin-bottom: 7px;', { text: r.kind }));
|
|
1775
|
-
card.appendChild(elS('div', 'font-size: 14px; line-height: 1.55; white-space: pre-wrap;', { text: r.text }));
|
|
1787
|
+
card.appendChild(elS('div', 'font-size: 14px; line-height: 1.55; white-space: pre-wrap;', { text: r.text, dir: 'auto' }));
|
|
1776
1788
|
const useBtn = elS('button', 'margin-top: 12px; border: 1px solid var(--ed-line); background: transparent; color: var(--ed-text); cursor: pointer; padding: 6px 11px; font-family: ui-monospace, monospace; font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase; transition: background 0.16s, color 0.16s, border-color 0.16s;', { type: 'button', text: r.action });
|
|
1777
1789
|
useBtn.addEventListener('mouseenter', () => { useBtn.style.background = 'var(--ed-accent)'; useBtn.style.color = 'var(--ed-accent-ink)'; useBtn.style.borderColor = 'var(--ed-accent)'; });
|
|
1778
1790
|
useBtn.addEventListener('mouseleave', () => { useBtn.style.background = 'transparent'; useBtn.style.color = 'var(--ed-text)'; useBtn.style.borderColor = 'var(--ed-line)'; });
|
package/src/render/block-body.js
CHANGED
|
@@ -15,6 +15,30 @@ function el(tag, style, attrs) {
|
|
|
15
15
|
|
|
16
16
|
function m(v) { return String(v == null ? '' : v); }
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Writing assistants (Grammarly, LanguageTool and friends) treat any
|
|
20
|
+
* `contenteditable` as a field of their own: they float a button over its
|
|
21
|
+
* bottom-right corner -- squarely on top of this editor's own block toolbar --
|
|
22
|
+
* and inject their highlight/overlay DOM into it, which `syncLiveEdit`
|
|
23
|
+
* (mailcraft-editor.js) then reads back as innerHTML and stores in the
|
|
24
|
+
* template, so their markup ends up in the exported email. Every one of them
|
|
25
|
+
* honours an opt-out attribute on the field itself, so each editable surface
|
|
26
|
+
* declares all of them here rather than repeating the list six times below.
|
|
27
|
+
* The `spellcheck="false"` that already rode along stays on every copy,
|
|
28
|
+
* editable or not, and `core/export.js` strips all of it back out on the way
|
|
29
|
+
* to the recipient.
|
|
30
|
+
*/
|
|
31
|
+
const NO_ASSIST = {
|
|
32
|
+
'data-gramm': 'false',
|
|
33
|
+
'data-gramm_editor': 'false',
|
|
34
|
+
'data-enable-grammarly': 'false',
|
|
35
|
+
'data-lt-active': 'false',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function editableAttrs(on) {
|
|
39
|
+
return on ? { contenteditable: 'true', spellcheck: 'false', ...NO_ASSIST } : { spellcheck: 'false' };
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
/** An explicit block font owns every rich descendant; an empty value deliberately leaves imported inline typography untouched. */
|
|
19
43
|
function overrideRichFont(root, fontFamily) {
|
|
20
44
|
if (!fontFamily) return;
|
|
@@ -38,7 +62,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
38
62
|
const edit = live;
|
|
39
63
|
const content = el('div', {
|
|
40
64
|
padding: pad(p), fontSize: p.size + 'px', lineHeight: p.lh, textAlign: p.align, fontWeight: p.weight, color: p.color || t.text, outline: 'none', fontFamily: p.fontFamily || t.font,
|
|
41
|
-
}, { ...attr,
|
|
65
|
+
}, { ...attr, ...editableAttrs(edit), html: m(p.html), 'data-focus-key': edit ? 'block:' + b.id : undefined });
|
|
42
66
|
// Imported email HTML keeps inline font-family declarations so an
|
|
43
67
|
// untouched block retains its source typography. Once the user chooses
|
|
44
68
|
// a block font, however, those nested declarations outrank inheritance
|
|
@@ -76,7 +100,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
76
100
|
// email pattern; the color falls back to the label color so a bare
|
|
77
101
|
// "outline thickness" bump looks right without a second step.
|
|
78
102
|
border: p.borderW ? p.borderW + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.borderColor || p.color) : '0',
|
|
79
|
-
}, { href: p.href,
|
|
103
|
+
}, { href: p.href, ...editableAttrs(live), text: p.label });
|
|
80
104
|
a.addEventListener('click', (e) => e.preventDefault());
|
|
81
105
|
// Button editing is deliberately plain: no focus-tracked `editing` state, no
|
|
82
106
|
// paste handling, no floating RTE toolbar -- only its label commits on blur.
|
|
@@ -147,7 +171,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
147
171
|
const safe = live
|
|
148
172
|
? raw.replace(/<style([^>]*)>([\s\S]*?)<\/style>/gi, (mm, at, css) => '<style' + at + '>' + ctx.scopeCss(css, '[data-mc-sheet]') + '</style>')
|
|
149
173
|
: raw;
|
|
150
|
-
const content = el('div', { padding: '6px 0', fontFamily: t.font, color: t.text, outline: 'none' }, { ...attr,
|
|
174
|
+
const content = el('div', { padding: '6px 0', fontFamily: t.font, color: t.text, outline: 'none' }, { ...attr, ...editableAttrs(edit), html: m(safe), 'data-focus-key': edit ? 'block:' + b.id : undefined });
|
|
151
175
|
if (edit) wireEditable(content, b, 'code', ctx, false);
|
|
152
176
|
if (!live) return content;
|
|
153
177
|
return wrapWithRte(b, ctx, edit && ctx.editingId === b.id, content);
|
|
@@ -187,7 +211,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
187
211
|
color: p.color || t.text, outline: 'none',
|
|
188
212
|
// An explicit per-block font beats the Condensed/Body style toggle.
|
|
189
213
|
fontFamily: p.fontFamily || (p.font === 'condensed' ? "'Arial Narrow', 'Helvetica Neue Condensed', Helvetica, Arial, sans-serif" : t.font),
|
|
190
|
-
}, { ...attr,
|
|
214
|
+
}, { ...attr, ...editableAttrs(edit), text: m(p.text), 'data-focus-key': edit ? 'block:' + b.id : undefined });
|
|
191
215
|
if (edit) wireEditable(head, b, 'text', ctx, true);
|
|
192
216
|
if (!live) return head;
|
|
193
217
|
return wrapWithRte(b, ctx, ctx.editingId === b.id, head);
|
|
@@ -217,7 +241,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
217
241
|
fontWeight: isHead ? '600' : '400', outline: 'none',
|
|
218
242
|
fontFamily: p.fontFamily || t.font,
|
|
219
243
|
letterSpacing: isHead ? '0.06em' : 'normal', textTransform: isHead ? 'uppercase' : 'none', fontSize: isHead ? p.size + 1 + 'px' : p.size + 'px',
|
|
220
|
-
}, {
|
|
244
|
+
}, { ...editableAttrs(edit), text: m(cell), 'data-focus-key': edit ? 'block:' + b.id + ':c' + ri + '-' + ci : undefined });
|
|
221
245
|
if (edit) {
|
|
222
246
|
// stopPropagation is load-bearing (a bubbled click would select
|
|
223
247
|
// via the block wrapper AND deselect via the workspace), but it
|
|
@@ -262,13 +286,32 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
262
286
|
minHeight: p.minH ? p.minH + 'px' : 'auto', maxWidth: p.maxW + '%', margin: p.align === 'center' ? '0 auto' : '0',
|
|
263
287
|
boxShadow: p.shadow ? '0 10px 30px rgba(29,31,32,0.12)' : 'none',
|
|
264
288
|
fontFamily: t.font, color: t.text, fontSize: '15px', lineHeight: '1.6',
|
|
265
|
-
}, { ...attr,
|
|
289
|
+
}, { ...attr, ...editableAttrs(edit), html: m(p.html), 'data-focus-key': edit ? 'block:' + b.id : undefined });
|
|
266
290
|
if (edit) wireEditable(box, b, 'html', ctx, false);
|
|
267
291
|
if (!live) return box;
|
|
268
292
|
return wrapWithRte(b, ctx, ctx.editingId === b.id, box);
|
|
269
293
|
}
|
|
270
294
|
case 'svg':
|
|
271
295
|
return el('div', { padding: p.py + 'px 0', textAlign: p.align }, { ...attr, html: '<span style="display:inline-block;width:' + p.width + '%">' + p.code + '</span>' });
|
|
296
|
+
// Dynamic-content markers. Editor furniture, never sent: the exporter
|
|
297
|
+
// emits the template tag itself at this position (core/export.js) and
|
|
298
|
+
// never reads this DOM back. The band renders in the preview too -- the
|
|
299
|
+
// preview shows the template the way it shows merge tokens, so the logic
|
|
300
|
+
// structure stays visible there rather than silently vanishing.
|
|
301
|
+
case 'condition':
|
|
302
|
+
case 'loop': {
|
|
303
|
+
const isLoop = b.type === 'loop';
|
|
304
|
+
const color = isLoop ? '#7c3aed' : '#0e7490';
|
|
305
|
+
const word = p.end ? (isLoop ? 'end loop' : 'end if') : (isLoop ? 'repeat each' : 'show if');
|
|
306
|
+
const band = el('div', {
|
|
307
|
+
display: 'flex', alignItems: 'center', gap: '8px', boxSizing: 'border-box',
|
|
308
|
+
border: '1.5px dashed ' + color, borderRadius: '7px', background: color + '14',
|
|
309
|
+
padding: '6px 12px', margin: '2px 0', color,
|
|
310
|
+
}, attr);
|
|
311
|
+
band.appendChild(el('span', { fontFamily: 'ui-monospace,monospace', fontSize: '9.5px', fontWeight: '700', letterSpacing: '0.12em', textTransform: 'uppercase', flex: 'none' }, { text: (p.end ? '⏶ ' : '⏷ ') + word }));
|
|
312
|
+
if (!p.end) band.appendChild(el('span', { fontFamily: 'ui-monospace,monospace', fontSize: '11.5px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, { text: '{{ ' + (p.expr || '…') + ' }}' }));
|
|
313
|
+
return band;
|
|
314
|
+
}
|
|
272
315
|
case 'codeblock':
|
|
273
316
|
return el('pre', { margin: '0', padding: p.pad + 'px', background: p.bg, color: p.color, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: p.size + 'px', lineHeight: '1.6', overflowX: 'auto', whiteSpace: 'pre' }, { ...attr, text: m(p.code) });
|
|
274
317
|
default:
|
package/src/render/canvas.js
CHANGED
|
@@ -169,10 +169,16 @@ export function renderDoc(core, live) {
|
|
|
169
169
|
// Lives only for this render's drag gesture(s) -- no core state, no re-render.
|
|
170
170
|
const rowDropTracker = { active: null };
|
|
171
171
|
|
|
172
|
+
// `dir="ltr"` pins the sheet to the document's own direction. The editor
|
|
173
|
+
// chrome mirrors under an RTL locale (`dir=rtl` on `#mc`), but the email
|
|
174
|
+
// being built is a separate document: exportHtml emits no `dir`, so mail
|
|
175
|
+
// clients render it LTR -- letting the chrome's direction cascade in here
|
|
176
|
+
// flipped bidi punctuation and default alignment on the canvas and made it
|
|
177
|
+
// disagree with the exported result.
|
|
172
178
|
const root = el('div', {
|
|
173
179
|
width: width + 'px', maxWidth: '100%', background: theme.contentBg, color: theme.text, fontFamily: theme.font,
|
|
174
180
|
transition: 'width 0.28s cubic-bezier(0.22,0.61,0.36,1), background 0.2s',
|
|
175
|
-
}, { 'data-mc-sheet': '1' });
|
|
181
|
+
}, { 'data-mc-sheet': '1', dir: 'ltr' });
|
|
176
182
|
|
|
177
183
|
const rowLines = [];
|
|
178
184
|
if (live) {
|
|
@@ -229,8 +235,31 @@ export function renderDoc(core, live) {
|
|
|
229
235
|
bWrap.addEventListener('dragstart', core.startDrag({ kind: 'move-block', id: b.id }));
|
|
230
236
|
bWrap.addEventListener('dragend', () => { core.drag = null; hideActive(rowDropTracker); hideActive(colTracker); });
|
|
231
237
|
bWrap.addEventListener('click', (e) => { e.stopPropagation(); core.select('block', b.id); });
|
|
238
|
+
// WebKit gives a `draggable="true"` ancestor priority over the
|
|
239
|
+
// selection machinery: a mousedown anywhere inside this wrapper is
|
|
240
|
+
// taken as the start of an element drag, so in Safari a double-click
|
|
241
|
+
// on block text never selects the word and pressing and sweeping
|
|
242
|
+
// across it never extends a selection. (Chromium and Gecko both
|
|
243
|
+
// special-case contenteditable descendants; WebKit does not.)
|
|
244
|
+
// Dropping the attribute for the length of a gesture that begins
|
|
245
|
+
// inside editable text hands those mousedowns back to the caret;
|
|
246
|
+
// restoring it on mouseup keeps drag-to-reorder working from every
|
|
247
|
+
// other part of the block -- including its padding, which is what a
|
|
248
|
+
// block is normally picked up by.
|
|
249
|
+
bWrap.addEventListener('mousedown', (e) => {
|
|
250
|
+
const target = e.target;
|
|
251
|
+
if (!target || target.nodeType !== 1 || !target.closest('[contenteditable="true"]')) return;
|
|
252
|
+
bWrap.draggable = false;
|
|
253
|
+
const view = bWrap.ownerDocument.defaultView;
|
|
254
|
+
if (view) view.addEventListener('mouseup', () => { bWrap.draggable = true; }, { once: true });
|
|
255
|
+
});
|
|
232
256
|
}
|
|
233
|
-
|
|
257
|
+
// Not while this block is being inline-edited: the pill sits at the
|
|
258
|
+
// block's top-right, flush against the floating RTE toolbar's bottom
|
|
259
|
+
// edge -- visually colliding with it, and putting its Delete button a
|
|
260
|
+
// few pixels from the RTE's own controls. The RTE stands in for it for
|
|
261
|
+
// the duration of the edit; it comes back on blur (still selected).
|
|
262
|
+
if (bSel && ctx.editingId !== b.id) bWrap.appendChild(toolbar(core, b.id, 'block', DEF(b.type).code));
|
|
234
263
|
bWrap.appendChild(blockBody(b, theme, live, ctx));
|
|
235
264
|
items.push(bWrap);
|
|
236
265
|
});
|