@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
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Order confirmed</title>
|
|
6
|
-
</head>
|
|
7
|
-
<body style="margin:0; padding:0; background-color:#f4f4f2;">
|
|
8
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f2;">
|
|
9
|
-
<tr>
|
|
10
|
-
<td align="center" style="padding:24px 12px;">
|
|
11
|
-
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
|
|
12
|
-
<tr>
|
|
13
|
-
<td style="padding:32px 36px 6px;">
|
|
14
|
-
<p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:12px; font-weight:bold; letter-spacing:0.16em; text-transform:uppercase; color:#2f9e44;">Order confirmed</p>
|
|
15
|
-
<h1 style="margin:0 0 10px; font-family:Arial, Helvetica, sans-serif; font-size:24px; line-height:1.25; color:#1a1a1a;">Thanks, {{ first_name }} — it’s on its way</h1>
|
|
16
|
-
<p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:14px; line-height:1.6; color:#4a4a4a;">Order <strong>#48213</strong>, placed today. We’ll email tracking as soon as the courier scans it — usually within one business day.</p>
|
|
17
|
-
</td>
|
|
18
|
-
</tr>
|
|
19
|
-
<tr>
|
|
20
|
-
<td style="padding:24px 36px 4px;">
|
|
21
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e6e6e2; ">
|
|
22
|
-
<tr>
|
|
23
|
-
<td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
|
|
24
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
25
|
-
<tr>
|
|
26
|
-
<td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Field jacket — olive, M</td>
|
|
27
|
-
<td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$148.00</td>
|
|
28
|
-
</tr>
|
|
29
|
-
</table>
|
|
30
|
-
</td>
|
|
31
|
-
</tr>
|
|
32
|
-
<tr>
|
|
33
|
-
<td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
|
|
34
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
35
|
-
<tr>
|
|
36
|
-
<td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Merino crew — charcoal, M</td>
|
|
37
|
-
<td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$85.00</td>
|
|
38
|
-
</tr>
|
|
39
|
-
</table>
|
|
40
|
-
</td>
|
|
41
|
-
</tr>
|
|
42
|
-
<tr>
|
|
43
|
-
<td style="padding:14px 18px; background-color:#fafaf8;">
|
|
44
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
45
|
-
<tr>
|
|
46
|
-
<td style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Shipping</td>
|
|
47
|
-
<td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Free</td>
|
|
48
|
-
</tr>
|
|
49
|
-
<tr>
|
|
50
|
-
<td style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">Total</td>
|
|
51
|
-
<td align="right" style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">$233.00</td>
|
|
52
|
-
</tr>
|
|
53
|
-
</table>
|
|
54
|
-
</td>
|
|
55
|
-
</tr>
|
|
56
|
-
</table>
|
|
57
|
-
</td>
|
|
58
|
-
</tr>
|
|
59
|
-
<tr>
|
|
60
|
-
<td align="center" style="padding:22px 36px 6px;">
|
|
61
|
-
<a href="https://example.com/orders/48213" style="display:inline-block; background-color:#1a1a1a; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:14px; font-weight:bold; text-decoration:none; padding:12px 30px; ">View your order</a>
|
|
62
|
-
</td>
|
|
63
|
-
</tr>
|
|
64
|
-
<tr>
|
|
65
|
-
<td style="padding:24px 36px 8px;">
|
|
66
|
-
<hr style="border:none; border-top:1px solid #e6e6e2; margin:0;" />
|
|
67
|
-
</td>
|
|
68
|
-
</tr>
|
|
69
|
-
<tr>
|
|
70
|
-
<td style="padding:10px 36px 30px;">
|
|
71
|
-
<p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:13px; line-height:1.6; color:#4a4a4a;">Need a different size? Exchanges are free for 30 days — start one from the order page above.</p>
|
|
72
|
-
<p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:11.5px; line-height:1.6; color:#9a9a94;">This receipt was sent to {{ email }}. <a href="{{ unsubscribe_url }}" style="color:#9a9a94;">Unsubscribe from marketing</a> — you will still receive order emails.</p>
|
|
73
|
-
</td>
|
|
74
|
-
</tr>
|
|
75
|
-
</table>
|
|
76
|
-
</td>
|
|
77
|
-
</tr>
|
|
78
|
-
</table>
|
|
79
|
-
</body>
|
|
80
|
-
</html>
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Order confirmed</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body style="margin:0; padding:0; background-color:#f4f4f2;">
|
|
8
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f2;">
|
|
9
|
+
<tr>
|
|
10
|
+
<td align="center" style="padding:24px 12px;">
|
|
11
|
+
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
|
|
12
|
+
<tr>
|
|
13
|
+
<td style="padding:32px 36px 6px;">
|
|
14
|
+
<p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:12px; font-weight:bold; letter-spacing:0.16em; text-transform:uppercase; color:#2f9e44;">Order confirmed</p>
|
|
15
|
+
<h1 style="margin:0 0 10px; font-family:Arial, Helvetica, sans-serif; font-size:24px; line-height:1.25; color:#1a1a1a;">Thanks, {{ first_name }} — it’s on its way</h1>
|
|
16
|
+
<p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:14px; line-height:1.6; color:#4a4a4a;">Order <strong>#48213</strong>, placed today. We’ll email tracking as soon as the courier scans it — usually within one business day.</p>
|
|
17
|
+
</td>
|
|
18
|
+
</tr>
|
|
19
|
+
<tr>
|
|
20
|
+
<td style="padding:24px 36px 4px;">
|
|
21
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e6e6e2; ">
|
|
22
|
+
<tr>
|
|
23
|
+
<td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
|
|
24
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
25
|
+
<tr>
|
|
26
|
+
<td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Field jacket — olive, M</td>
|
|
27
|
+
<td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$148.00</td>
|
|
28
|
+
</tr>
|
|
29
|
+
</table>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
<tr>
|
|
33
|
+
<td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
|
|
34
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
35
|
+
<tr>
|
|
36
|
+
<td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Merino crew — charcoal, M</td>
|
|
37
|
+
<td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$85.00</td>
|
|
38
|
+
</tr>
|
|
39
|
+
</table>
|
|
40
|
+
</td>
|
|
41
|
+
</tr>
|
|
42
|
+
<tr>
|
|
43
|
+
<td style="padding:14px 18px; background-color:#fafaf8;">
|
|
44
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
45
|
+
<tr>
|
|
46
|
+
<td style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Shipping</td>
|
|
47
|
+
<td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Free</td>
|
|
48
|
+
</tr>
|
|
49
|
+
<tr>
|
|
50
|
+
<td style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">Total</td>
|
|
51
|
+
<td align="right" style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">$233.00</td>
|
|
52
|
+
</tr>
|
|
53
|
+
</table>
|
|
54
|
+
</td>
|
|
55
|
+
</tr>
|
|
56
|
+
</table>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr>
|
|
60
|
+
<td align="center" style="padding:22px 36px 6px;">
|
|
61
|
+
<a href="https://example.com/orders/48213" style="display:inline-block; background-color:#1a1a1a; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:14px; font-weight:bold; text-decoration:none; padding:12px 30px; ">View your order</a>
|
|
62
|
+
</td>
|
|
63
|
+
</tr>
|
|
64
|
+
<tr>
|
|
65
|
+
<td style="padding:24px 36px 8px;">
|
|
66
|
+
<hr style="border:none; border-top:1px solid #e6e6e2; margin:0;" />
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
<tr>
|
|
70
|
+
<td style="padding:10px 36px 30px;">
|
|
71
|
+
<p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:13px; line-height:1.6; color:#4a4a4a;">Need a different size? Exchanges are free for 30 days — start one from the order page above.</p>
|
|
72
|
+
<p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:11.5px; line-height:1.6; color:#9a9a94;">This receipt was sent to {{ email }}. <a href="{{ unsubscribe_url }}" style="color:#9a9a94;">Unsubscribe from marketing</a> — you will still receive order emails.</p>
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
</table>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
</table>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
package/examples/vanilla.html
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
palette (src/render/style.js) so the demo reads as one application rather
|
|
14
14
|
than a widget dropped into a stranger's page. */
|
|
15
15
|
:root {
|
|
16
|
+
/* Pinned per theme, not left at `light dark`: it is what decides whether
|
|
17
|
+
the browser paints this page a light or a dark scrollbar. */
|
|
18
|
+
color-scheme: light;
|
|
16
19
|
--brand: #0065b3;
|
|
17
20
|
--bg: #ffffff;
|
|
18
21
|
--surface: #f8fafc;
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
/* One attribute drives both: the same switch that sets the editor's `theme`
|
|
28
31
|
repaints this page. Host-owned light/dark, demonstrated rather than
|
|
29
32
|
described. */
|
|
33
|
+
:root:has(body[data-theme="dark"]) { color-scheme: dark; }
|
|
30
34
|
body[data-theme="dark"] {
|
|
31
35
|
--bg: #0b1120;
|
|
32
36
|
--surface: #111827;
|
|
@@ -129,7 +133,8 @@
|
|
|
129
133
|
<nav class="menu" aria-label="Primary">
|
|
130
134
|
<a id="docs-link" href="../DOCS.md">Documentation</a>
|
|
131
135
|
<a id="readme-link" href="../README.md">Overview</a>
|
|
132
|
-
|
|
136
|
+
<!-- No GitHub link while the repository is private -- it 404s for anyone
|
|
137
|
+
outside the org. Restore it when the repo goes public. -->
|
|
133
138
|
<a href="https://www.npmjs.com/package/@seliseblocks/mailcraft" rel="noopener">npm</a>
|
|
134
139
|
</nav>
|
|
135
140
|
|
|
@@ -1449,6 +1454,8 @@
|
|
|
1449
1454
|
const next = editor.getAttribute('theme') === 'dark' ? 'light' : 'dark';
|
|
1450
1455
|
editor.setAttribute('theme', next);
|
|
1451
1456
|
document.body.dataset.theme = next;
|
|
1457
|
+
// :has() covers modern browsers; this covers the rest, and costs one line.
|
|
1458
|
+
document.documentElement.style.colorScheme = next;
|
|
1452
1459
|
themeBtn.textContent = next === 'dark' ? 'Switch to light' : 'Switch to dark';
|
|
1453
1460
|
});
|
|
1454
1461
|
|
|
@@ -1541,32 +1548,244 @@
|
|
|
1541
1548
|
// calls below at S3, a DMS, a CDN or your own API and the editor is
|
|
1542
1549
|
// unchanged.
|
|
1543
1550
|
//
|
|
1544
|
-
//
|
|
1551
|
+
// This demo wires a real one. There is no server behind a static page, so
|
|
1552
|
+
// the "backend" is IndexedDB in your own browser — but everything the editor
|
|
1553
|
+
// is allowed to see about it behaves like a service: round-trip latency,
|
|
1554
|
+
// folders and search resolved on the far side, cursor-paged listings,
|
|
1555
|
+
// uploads that are still there after a reload, deletes that stay deleted,
|
|
1556
|
+
// and cancellation honoured. Replace `demoBackend` with `fetch` calls to
|
|
1557
|
+
// your API and the provider underneath it does not change a line.
|
|
1545
1558
|
//
|
|
1546
|
-
// editor
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
|
|
1551
|
-
//
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1559
|
+
// The editor ships no files of its own: with nothing wired here the library
|
|
1560
|
+
// opens empty and stays that way until someone drops something in. Every
|
|
1561
|
+
// file in the Assets modal is defined below — host content, in host code,
|
|
1562
|
+
// exactly where a real integration keeps it.
|
|
1563
|
+
|
|
1564
|
+
// ---- the pretend backend ------------------------------------------------
|
|
1565
|
+
|
|
1566
|
+
const demoBackend = (() => {
|
|
1567
|
+
const DB_NAME = 'mailcraft-demo-assets';
|
|
1568
|
+
const STORE = 'files';
|
|
1569
|
+
const PAGE_SIZE = 8; // small on purpose: it makes "Load more" real
|
|
1570
|
+
const LATENCY = 240; // roughly what one round trip to a DMS costs
|
|
1571
|
+
|
|
1572
|
+
// Folders are the backend's, not the editor's. Ids are what travel over
|
|
1573
|
+
// the wire; the names are only ever display strings.
|
|
1574
|
+
const FOLDERS = [
|
|
1575
|
+
{ id: 'brand', name: 'Brand' },
|
|
1576
|
+
{ id: 'product', name: 'Product' },
|
|
1577
|
+
{ id: 'photography', name: 'Photography' },
|
|
1578
|
+
{ id: 'uploads', name: 'Uploads' },
|
|
1579
|
+
];
|
|
1580
|
+
const folderName = (id) => (FOLDERS.find((f) => f.id === id) || {}).name || '';
|
|
1581
|
+
|
|
1582
|
+
// The demo's own placeholder images, as SVG data URIs so the catalogue
|
|
1583
|
+
// needs no binaries next to this file. A real backend hands back durable
|
|
1584
|
+
// https URLs instead — see the note on `url` at the bottom.
|
|
1585
|
+
const ph = (label, w, ht) => 'data:image/svg+xml;utf8,' + encodeURIComponent(
|
|
1586
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">'
|
|
1587
|
+
+ '<defs><pattern id="p" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">'
|
|
1588
|
+
+ '<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>'
|
|
1589
|
+
+ '<rect width="100%" height="100%" fill="url(#p)"/>'
|
|
1590
|
+
+ '<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>'
|
|
1591
|
+
+ '<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="'
|
|
1592
|
+
+ Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
|
|
1593
|
+
).replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
1594
|
+
|
|
1595
|
+
// Fourteen files across three folders: enough that a listing pages, and
|
|
1596
|
+
// enough that a search narrows something.
|
|
1597
|
+
const CATALOGUE = [
|
|
1598
|
+
['logo-lockup.png', 'brand', 480, 180],
|
|
1599
|
+
['logo-mark.png', 'brand', 240, 240],
|
|
1600
|
+
['logo-wordmark.png', 'brand', 520, 140],
|
|
1601
|
+
['jacket-03-front.jpg', 'product', 520, 360],
|
|
1602
|
+
['jacket-03-detail.jpg', 'product', 520, 360],
|
|
1603
|
+
['jacket-03-lining.jpg', 'product', 520, 360],
|
|
1604
|
+
['boots-pair.jpg', 'product', 520, 360],
|
|
1605
|
+
['boots-sole.jpg', 'product', 520, 360],
|
|
1606
|
+
['satchel-tan.jpg', 'product', 520, 360],
|
|
1607
|
+
['hero-workshop.jpg', 'photography', 600, 340],
|
|
1608
|
+
['hero-storefront.jpg', 'photography', 600, 340],
|
|
1609
|
+
['texture-waxed.jpg', 'photography', 600, 240],
|
|
1610
|
+
['texture-canvas.jpg', 'photography', 600, 240],
|
|
1611
|
+
['team-bench.jpg', 'photography', 600, 400],
|
|
1612
|
+
];
|
|
1613
|
+
|
|
1614
|
+
// IndexedDB is unavailable on a `file://` page in Chrome, and this file is
|
|
1615
|
+
// meant to survive being double-clicked. The in-memory shim keeps every
|
|
1616
|
+
// path below identical; the only thing lost is "survives a reload".
|
|
1617
|
+
let memory = null;
|
|
1618
|
+
let dbPromise = null;
|
|
1619
|
+
function db() {
|
|
1620
|
+
if (memory) return Promise.resolve(null);
|
|
1621
|
+
if (!dbPromise) {
|
|
1622
|
+
dbPromise = new Promise((resolve) => {
|
|
1623
|
+
let req;
|
|
1624
|
+
try { req = indexedDB.open(DB_NAME, 1); } catch (e) { memory = new Map(); return resolve(null); }
|
|
1625
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE, { keyPath: 'id' });
|
|
1626
|
+
req.onsuccess = () => resolve(req.result);
|
|
1627
|
+
req.onerror = () => { memory = new Map(); resolve(null); };
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
return dbPromise;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async function readAll() {
|
|
1634
|
+
const conn = await db();
|
|
1635
|
+
if (!conn) return Array.from(memory.values());
|
|
1636
|
+
return new Promise((resolve, reject) => {
|
|
1637
|
+
const req = conn.transaction(STORE, 'readonly').objectStore(STORE).getAll();
|
|
1638
|
+
req.onsuccess = () => resolve(req.result || []);
|
|
1639
|
+
req.onerror = () => reject(req.error);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async function write(rec) {
|
|
1644
|
+
const conn = await db();
|
|
1645
|
+
if (!conn) { memory.set(rec.id, rec); return rec; }
|
|
1646
|
+
return new Promise((resolve, reject) => {
|
|
1647
|
+
const t = conn.transaction(STORE, 'readwrite');
|
|
1648
|
+
t.objectStore(STORE).put(rec);
|
|
1649
|
+
t.oncomplete = () => resolve(rec);
|
|
1650
|
+
t.onerror = () => reject(t.error);
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
async function drop(id) {
|
|
1655
|
+
const conn = await db();
|
|
1656
|
+
if (!conn) { memory.delete(id); return; }
|
|
1657
|
+
return new Promise((resolve, reject) => {
|
|
1658
|
+
const t = conn.transaction(STORE, 'readwrite');
|
|
1659
|
+
t.objectStore(STORE).delete(id);
|
|
1660
|
+
t.oncomplete = () => resolve();
|
|
1661
|
+
t.onerror = () => reject(t.error);
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// First run only: a real backend already has the customer's files in it.
|
|
1666
|
+
let seeded = null;
|
|
1667
|
+
function seed() {
|
|
1668
|
+
if (!seeded) {
|
|
1669
|
+
seeded = readAll().then(async (rows) => {
|
|
1670
|
+
if (rows.length) return;
|
|
1671
|
+
const t0 = Date.now() - CATALOGUE.length * 1000;
|
|
1672
|
+
for (let i = 0; i < CATALOGUE.length; i++) {
|
|
1673
|
+
const name = CATALOGUE[i][0];
|
|
1674
|
+
const folderId = CATALOGUE[i][1];
|
|
1675
|
+
const w = CATALOGUE[i][2];
|
|
1676
|
+
const ht = CATALOGUE[i][3];
|
|
1677
|
+
const url = ph(name.replace(/\.[a-z]+$/i, '').replace(/-/g, ' '), w, ht);
|
|
1678
|
+
await write({ id: 'seed-' + i, name: name, folderId: folderId, url: url, w: w, ht: ht, size: url.length, createdAt: t0 + i * 1000 });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
return seeded;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/** Latency you can cancel: an aborted request rejects the way `fetch` does. */
|
|
1686
|
+
function nap(ms, signal) {
|
|
1687
|
+
return new Promise((resolve, reject) => {
|
|
1688
|
+
if (signal && signal.aborted) return reject(new DOMException('Aborted', 'AbortError'));
|
|
1689
|
+
const id = setTimeout(resolve, ms);
|
|
1690
|
+
if (signal) signal.addEventListener('abort', () => { clearTimeout(id); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const readAsDataUrl = (file) => new Promise((resolve, reject) => {
|
|
1695
|
+
const fr = new FileReader();
|
|
1696
|
+
fr.onload = () => resolve(fr.result);
|
|
1697
|
+
fr.onerror = () => reject(new Error('the file could not be read'));
|
|
1698
|
+
fr.readAsDataURL(file);
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
return {
|
|
1702
|
+
folders: async () => { await nap(LATENCY / 2); return FOLDERS.map((f) => ({ id: f.id, name: f.name })); },
|
|
1703
|
+
|
|
1704
|
+
// Folder, search and paging are all resolved here, on the far side of the
|
|
1705
|
+
// wire — which is exactly why the editor stops filtering locally the
|
|
1706
|
+
// moment a provider is assigned. A cursor is opaque to it: this one is an
|
|
1707
|
+
// offset, a real one is usually a continuation token.
|
|
1708
|
+
async list(q) {
|
|
1709
|
+
await seed();
|
|
1710
|
+
await nap(LATENCY, q.signal);
|
|
1711
|
+
const term = (q.query || '').trim().toLowerCase();
|
|
1712
|
+
const rows = (await readAll())
|
|
1713
|
+
.filter((r) => (!q.folderId || r.folderId === q.folderId) && (!term || r.name.toLowerCase().includes(term)))
|
|
1714
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
1715
|
+
const from = Number(q.cursor) || 0;
|
|
1716
|
+
return {
|
|
1717
|
+
items: rows.slice(from, from + PAGE_SIZE).map((r) => ({
|
|
1718
|
+
id: r.id, name: r.name, url: r.url,
|
|
1719
|
+
folder: folderName(r.folderId), folderId: r.folderId,
|
|
1720
|
+
w: r.w, ht: r.ht, size: r.size,
|
|
1721
|
+
})),
|
|
1722
|
+
cursor: from + PAGE_SIZE < rows.length ? String(from + PAGE_SIZE) : null,
|
|
1723
|
+
};
|
|
1724
|
+
},
|
|
1725
|
+
|
|
1726
|
+
// The editor has already validated the file against `storageLimits` and
|
|
1727
|
+
// measured it, so `width`/`height` arrive filled in. Uploading from "All
|
|
1728
|
+
// files" means no folder was chosen — a backend still has to put the
|
|
1729
|
+
// bytes somewhere, and "Uploads" is this one's answer.
|
|
1730
|
+
async upload(file, opts) {
|
|
1731
|
+
const url = await readAsDataUrl(file);
|
|
1732
|
+
await nap(LATENCY * 2, opts.signal);
|
|
1733
|
+
const target = opts.folderId || 'uploads';
|
|
1734
|
+
const rec = {
|
|
1735
|
+
id: 'up-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7),
|
|
1736
|
+
name: file.name, folderId: target, url: url,
|
|
1737
|
+
w: opts.width || 0, ht: opts.height || 0, size: file.size, createdAt: Date.now(),
|
|
1738
|
+
};
|
|
1739
|
+
await write(rec);
|
|
1740
|
+
return { id: rec.id, name: rec.name, url: rec.url, folder: folderName(target), folderId: target, w: rec.w, ht: rec.ht, size: rec.size };
|
|
1741
|
+
},
|
|
1742
|
+
|
|
1743
|
+
async remove(asset) {
|
|
1744
|
+
await nap(LATENCY);
|
|
1745
|
+
await drop(asset.id);
|
|
1746
|
+
},
|
|
1747
|
+
|
|
1748
|
+
/** Not part of the contract — a console escape hatch for this demo. */
|
|
1749
|
+
async reset() {
|
|
1750
|
+
const rows = await readAll();
|
|
1751
|
+
for (let i = 0; i < rows.length; i++) await drop(rows[i].id);
|
|
1752
|
+
seeded = null;
|
|
1753
|
+
await seed();
|
|
1754
|
+
},
|
|
1755
|
+
};
|
|
1756
|
+
})();
|
|
1757
|
+
|
|
1758
|
+
// The provider itself: four thin methods over whatever the backend is. This
|
|
1759
|
+
// is the entire integration — no editor internals, no imports, no build step.
|
|
1760
|
+
const demoStorage = {
|
|
1761
|
+
folders: () => demoBackend.folders(),
|
|
1762
|
+
list: (q) => demoBackend.list(q),
|
|
1763
|
+
upload: (file, opts) => demoBackend.upload(file, opts),
|
|
1764
|
+
remove: (asset) => demoBackend.remove(asset),
|
|
1765
|
+
};
|
|
1766
|
+
editor.storageProvider = demoStorage;
|
|
1767
|
+
|
|
1768
|
+
// Handy while poking at the demo: `mailcraftDemo.reset()` empties the
|
|
1769
|
+
// library, re-seeds it, and re-lists it. Re-assigning the provider is how a
|
|
1770
|
+
// host tells the editor to start over — the same call the line above makes.
|
|
1771
|
+
window.mailcraftDemo = {
|
|
1772
|
+
reset: () => demoBackend.reset().then(() => { editor.storageProvider = demoStorage; }),
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
// Against a real API the same four methods are usually one-liners:
|
|
1776
|
+
//
|
|
1777
|
+
// list: ({ folderId, cursor, query, signal }) => myApi.listImages({ folder: folderId, cursor, q: query, signal })
|
|
1778
|
+
// upload: (file, { folderId, width, height, signal }) => myApi.upload(file, folderId, { signal })
|
|
1779
|
+
// folders: () => myApi.folders(), // optional
|
|
1780
|
+
// remove: (asset) => myApi.delete(asset.id), // optional
|
|
1781
|
+
//
|
|
1782
|
+
// — each response mapped onto `{ id, name, url, folder, folderId, w, ht, size }`.
|
|
1566
1783
|
//
|
|
1567
1784
|
// The one hard requirement is on `url`: it has to still resolve after the
|
|
1568
1785
|
// campaign is sent. A URL that expires in an hour produces mail whose images
|
|
1569
|
-
// are already broken when it lands.
|
|
1786
|
+
// are already broken when it lands. This demo cheats it with data URIs, which
|
|
1787
|
+
// no email client will render — a real integration returns the durable https
|
|
1788
|
+
// URL its CDN serves.
|
|
1570
1789
|
//
|
|
1571
1790
|
// README.md has the full contract.
|
|
1572
1791
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seliseblocks/mailcraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Framework-agnostic drag-and-drop email template editor, packaged as a zero-dependency Web Component.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SELISE Digital Platforms",
|
|
@@ -30,10 +30,14 @@
|
|
|
30
30
|
],
|
|
31
31
|
"main": "./src/index.js",
|
|
32
32
|
"module": "./src/index.js",
|
|
33
|
+
"types": "./types/index.d.ts",
|
|
33
34
|
"unpkg": "./dist/mailcraft-editor.bundle.js",
|
|
34
35
|
"jsdelivr": "./dist/mailcraft-editor.bundle.js",
|
|
35
36
|
"exports": {
|
|
36
|
-
".":
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./types/index.d.ts",
|
|
39
|
+
"default": "./src/index.js"
|
|
40
|
+
},
|
|
37
41
|
"./dist/*": "./dist/*",
|
|
38
42
|
"./src/*": "./src/*",
|
|
39
43
|
"./package.json": "./package.json"
|
|
@@ -41,6 +45,7 @@
|
|
|
41
45
|
"files": [
|
|
42
46
|
"src",
|
|
43
47
|
"dist",
|
|
48
|
+
"types",
|
|
44
49
|
"examples",
|
|
45
50
|
"README.md",
|
|
46
51
|
"DOCS.md",
|
package/src/core/assets.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{ id: uid(), name: 'boots-pair.jpg', url: PH('boots pair', 520, 360), folder: 'Product', w: 520, ht: 360, size: 88000 },
|
|
12
|
-
{ id: uid(), name: 'texture-waxed.jpg', url: PH('waxed texture', 600, 240), folder: 'Photography', w: 600, ht: 240, size: 64000 },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export const KB = (n) => (n > 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.round(n / 1024) + ' KB');
|
|
1
|
+
/**
|
|
2
|
+
* Asset-size formatting.
|
|
3
|
+
*
|
|
4
|
+
* No file list lives here. The library is the host's: with a `storageProvider`
|
|
5
|
+
* wired the files come from the backend, and without one it starts empty and
|
|
6
|
+
* holds only what the user drops in this session. Shipping a fabricated
|
|
7
|
+
* catalogue of "brand" and "product" files meant every integrator's first run
|
|
8
|
+
* showed six files nobody in their organisation had ever uploaded.
|
|
9
|
+
*/
|
|
10
|
+
export const KB = (n) => (n > 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.round(n / 1024) + ' KB');
|