@seliseblocks/mailcraft 0.1.1 → 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 +238 -10
- 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 +234 -22
- 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
|
@@ -1548,32 +1548,244 @@
|
|
|
1548
1548
|
// calls below at S3, a DMS, a CDN or your own API and the editor is
|
|
1549
1549
|
// unchanged.
|
|
1550
1550
|
//
|
|
1551
|
-
//
|
|
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.
|
|
1552
1558
|
//
|
|
1553
|
-
// editor
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1558
|
-
//
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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 }`.
|
|
1573
1783
|
//
|
|
1574
1784
|
// The one hard requirement is on `url`: it has to still resolve after the
|
|
1575
1785
|
// campaign is sent. A URL that expires in an hour produces mail whose images
|
|
1576
|
-
// 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.
|
|
1577
1789
|
//
|
|
1578
1790
|
// README.md has the full contract.
|
|
1579
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');
|