@seliseblocks/mailcraft 0.1.1 → 0.2.1
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 +243 -16
- package/README.md +15 -6
- package/README.md.txt +135 -0
- package/dist/mailcraft-editor.bundle.js +87 -47
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/activate-your-account.html +1 -1
- package/examples/templates/back-in-stock.html +1 -1
- package/examples/templates/cart-left-behind.html +1 -1
- package/examples/templates/community-giveaway.html +1 -1
- package/examples/templates/frontend-futures-invite.html +1 -1
- package/examples/templates/give-25-get-25.html +1 -1
- package/examples/templates/invoice-paid.html +1 -1
- package/examples/templates/meet-nova-launch.html +1 -1
- package/examples/templates/mega-weekend-sale.html +1 -1
- package/examples/templates/order-confirmed.html +80 -80
- package/examples/templates/rate-your-headphones.html +1 -1
- package/examples/templates/reset-your-password.html +1 -1
- package/examples/templates/thankyou-promo-code.html +1 -1
- package/examples/templates/the-sunday-brief.html +1 -1
- package/examples/templates/welcome-to-your-workspace.html +1 -1
- package/examples/templates/your-order-shipped.html +1 -1
- package/examples/templates/your-password-was-changed.html +1 -1
- package/examples/templates/your-signin-code.html +1 -1
- package/examples/vanilla.html +261 -47
- package/package.json +7 -2
- package/src/core/assets.js +10 -15
- package/src/core/binder.js +140 -118
- package/src/core/blocks.js +14 -1
- package/src/core/css-cascade.js +117 -117
- package/src/core/editor-core.js +1540 -1492
- package/src/core/export.js +158 -55
- package/src/core/i18n/ar.js +218 -177
- package/src/core/i18n/bg.js +195 -152
- package/src/core/i18n/bn.js +217 -176
- package/src/core/i18n/ca.js +195 -152
- package/src/core/i18n/cs.js +195 -152
- package/src/core/i18n/da.js +195 -152
- package/src/core/i18n/de-CH.js +195 -152
- package/src/core/i18n/de.js +195 -152
- package/src/core/i18n/dz.js +220 -179
- package/src/core/i18n/el.js +195 -152
- package/src/core/i18n/en.js +3 -11
- package/src/core/i18n/es.js +195 -152
- package/src/core/i18n/et.js +195 -152
- package/src/core/i18n/fi.js +195 -152
- package/src/core/i18n/fr.js +195 -152
- package/src/core/i18n/hr.js +195 -152
- package/src/core/i18n/hu.js +195 -152
- package/src/core/i18n/index.js +83 -83
- package/src/core/i18n/it.js +195 -152
- package/src/core/i18n/lt.js +195 -152
- package/src/core/i18n/lv.js +195 -152
- package/src/core/i18n/nb.js +195 -152
- package/src/core/i18n/nl.js +195 -152
- package/src/core/i18n/pl.js +195 -152
- package/src/core/i18n/pt.js +195 -152
- package/src/core/i18n/ro.js +195 -152
- package/src/core/i18n/ru.js +195 -152
- package/src/core/i18n/sk.js +195 -152
- package/src/core/i18n/sl.js +195 -152
- package/src/core/i18n/sv.js +195 -152
- package/src/core/i18n/tables.js +50 -50
- package/src/core/i18n/tr.js +195 -152
- package/src/core/i18n/uk.js +195 -152
- package/src/core/icons.js +237 -235
- package/src/core/ids.js +1 -1
- package/src/core/import-html.js +1047 -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 +189 -184
- package/src/core/storage.js +85 -85
- package/src/core/theme.js +15 -1
- package/src/core/variables.js +11 -11
- package/src/index.js +9 -9
- package/src/mailcraft-editor.js +64 -25
- package/src/render/block-body.js +49 -6
- package/src/render/canvas.js +74 -8
- package/src/render/fields.js +619 -588
- package/src/render/focus-preserve.js +158 -158
- package/src/render/rte.js +241 -212
- package/src/render/screenshot.js +138 -132
- package/src/render/story.js +415 -415
- package/src/render/style.js +44 -8
- package/types/index.d.ts +422 -0
package/examples/vanilla.html
CHANGED
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
<body style="margin:0; padding:0; background-color:#eef2f6;">
|
|
219
219
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
|
|
220
220
|
<tr>
|
|
221
|
-
<td align="center" style="padding:
|
|
221
|
+
<td align="center" style="padding:0;">
|
|
222
222
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
|
|
223
223
|
<tr>
|
|
224
224
|
<td align="center" style="background-color:#0065b3; padding:22px 36px;">
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
<body style="margin:0; padding:0; background-color:#faf7f2;">
|
|
275
275
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#faf7f2;">
|
|
276
276
|
<tr>
|
|
277
|
-
<td align="center" style="padding:
|
|
277
|
+
<td align="center" style="padding:0;">
|
|
278
278
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
|
|
279
279
|
<tr>
|
|
280
280
|
<td align="center" style="padding:36px 36px 8px;">
|
|
@@ -339,7 +339,7 @@
|
|
|
339
339
|
<body style="margin:0; padding:0; background-color:#f6f6f8;">
|
|
340
340
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f6f6f8;">
|
|
341
341
|
<tr>
|
|
342
|
-
<td align="center" style="padding:
|
|
342
|
+
<td align="center" style="padding:0;">
|
|
343
343
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
|
|
344
344
|
<tr>
|
|
345
345
|
<td align="center" style="padding:34px 36px 8px;">
|
|
@@ -415,7 +415,7 @@
|
|
|
415
415
|
<body style="margin:0; padding:0; background-color:#0c1226;">
|
|
416
416
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0c1226;">
|
|
417
417
|
<tr>
|
|
418
|
-
<td align="center" style="padding:
|
|
418
|
+
<td align="center" style="padding:0;">
|
|
419
419
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#131b36; ">
|
|
420
420
|
<tr>
|
|
421
421
|
<td align="center" style="height:10px; line-height:10px; font-size:0; padding:0;">
|
|
@@ -498,7 +498,7 @@
|
|
|
498
498
|
<body style="margin:0; padding:0; background-color:#0e0c1a;">
|
|
499
499
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0e0c1a;">
|
|
500
500
|
<tr>
|
|
501
|
-
<td align="center" style="padding:
|
|
501
|
+
<td align="center" style="padding:0;">
|
|
502
502
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#171332; ">
|
|
503
503
|
<tr>
|
|
504
504
|
<td align="center" style="padding:12px 36px 0;">
|
|
@@ -569,7 +569,7 @@
|
|
|
569
569
|
<body style="margin:0; padding:0; background-color:#0f1c17;">
|
|
570
570
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f1c17;">
|
|
571
571
|
<tr>
|
|
572
|
-
<td align="center" style="padding:
|
|
572
|
+
<td align="center" style="padding:0;">
|
|
573
573
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#16352a; ">
|
|
574
574
|
<tr>
|
|
575
575
|
<td align="center" style="background-color:#1e7a4f; padding:30px 32px 26px; ">
|
|
@@ -640,7 +640,7 @@
|
|
|
640
640
|
<body style="margin:0; padding:0; background-color:#f4f6f9;">
|
|
641
641
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f6f9;">
|
|
642
642
|
<tr>
|
|
643
|
-
<td align="center" style="padding:
|
|
643
|
+
<td align="center" style="padding:0;">
|
|
644
644
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
|
|
645
645
|
<tr>
|
|
646
646
|
<td style="padding:30px 36px 10px;">
|
|
@@ -719,7 +719,7 @@
|
|
|
719
719
|
<body style="margin:0; padding:0; background-color:#0b1020;">
|
|
720
720
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0b1020;">
|
|
721
721
|
<tr>
|
|
722
|
-
<td align="center" style="padding:
|
|
722
|
+
<td align="center" style="padding:0;">
|
|
723
723
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#111832; ">
|
|
724
724
|
<tr>
|
|
725
725
|
<td align="center" style="background-color:#0065b3; padding:34px 32px 30px; ">
|
|
@@ -790,7 +790,7 @@
|
|
|
790
790
|
<body style="margin:0; padding:0; background-color:#ffffff;">
|
|
791
791
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#ffffff;">
|
|
792
792
|
<tr>
|
|
793
|
-
<td align="center" style="padding:
|
|
793
|
+
<td align="center" style="padding:0;">
|
|
794
794
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#101010;">
|
|
795
795
|
<tr>
|
|
796
796
|
<td style="height:6px; line-height:6px; font-size:0; background-color:#d9f24f;"> </td>
|
|
@@ -849,7 +849,7 @@
|
|
|
849
849
|
<body style="margin:0; padding:0; background-color:#f4f4f2;">
|
|
850
850
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f2;">
|
|
851
851
|
<tr>
|
|
852
|
-
<td align="center" style="padding:
|
|
852
|
+
<td align="center" style="padding:0;">
|
|
853
853
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
|
|
854
854
|
<tr>
|
|
855
855
|
<td style="padding:32px 36px 6px;">
|
|
@@ -931,7 +931,7 @@
|
|
|
931
931
|
<body style="margin:0; padding:0; background-color:#f2f4f8;">
|
|
932
932
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f2f4f8;">
|
|
933
933
|
<tr>
|
|
934
|
-
<td align="center" style="padding:
|
|
934
|
+
<td align="center" style="padding:0;">
|
|
935
935
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
|
|
936
936
|
<tr>
|
|
937
937
|
<td align="center" style="padding:36px 36px 10px;">
|
|
@@ -1004,7 +1004,7 @@
|
|
|
1004
1004
|
<body style="margin:0; padding:0; background-color:#eef2f6;">
|
|
1005
1005
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
|
|
1006
1006
|
<tr>
|
|
1007
|
-
<td align="center" style="padding:
|
|
1007
|
+
<td align="center" style="padding:0;">
|
|
1008
1008
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
|
|
1009
1009
|
<tr>
|
|
1010
1010
|
<td align="center" style="background-color:#0065b3; padding:22px 36px;">
|
|
@@ -1060,7 +1060,7 @@
|
|
|
1060
1060
|
<body style="margin:0; padding:0; background-color:#fdf8f3;">
|
|
1061
1061
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fdf8f3;">
|
|
1062
1062
|
<tr>
|
|
1063
|
-
<td align="center" style="padding:
|
|
1063
|
+
<td align="center" style="padding:0;">
|
|
1064
1064
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
|
|
1065
1065
|
<tr>
|
|
1066
1066
|
<td align="center" style="padding:38px 36px 8px;">
|
|
@@ -1130,7 +1130,7 @@
|
|
|
1130
1130
|
<body style="margin:0; padding:0; background-color:#f1f0ec;">
|
|
1131
1131
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f1f0ec;">
|
|
1132
1132
|
<tr>
|
|
1133
|
-
<td align="center" style="padding:
|
|
1133
|
+
<td align="center" style="padding:0;">
|
|
1134
1134
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
|
|
1135
1135
|
<tr>
|
|
1136
1136
|
<td style="background-color:#1c1b18; padding:26px 32px;">
|
|
@@ -1179,7 +1179,7 @@
|
|
|
1179
1179
|
<body style="margin:0; padding:0; background-color:#eef2f6;">
|
|
1180
1180
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
|
|
1181
1181
|
<tr>
|
|
1182
|
-
<td align="center" style="padding:
|
|
1182
|
+
<td align="center" style="padding:0;">
|
|
1183
1183
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
|
|
1184
1184
|
<tr>
|
|
1185
1185
|
<td style="height:5px; line-height:5px; font-size:0; background-color:#0065b3; "> </td>
|
|
@@ -1243,7 +1243,7 @@
|
|
|
1243
1243
|
<body style="margin:0; padding:0; background-color:#eef2f6;">
|
|
1244
1244
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
|
|
1245
1245
|
<tr>
|
|
1246
|
-
<td align="center" style="padding:
|
|
1246
|
+
<td align="center" style="padding:0;">
|
|
1247
1247
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
|
|
1248
1248
|
<tr>
|
|
1249
1249
|
<td align="center" style="background-color:#0065b3; padding:22px 36px;">
|
|
@@ -1319,7 +1319,7 @@
|
|
|
1319
1319
|
<body style="margin:0; padding:0; background-color:#eef2f6;">
|
|
1320
1320
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
|
|
1321
1321
|
<tr>
|
|
1322
|
-
<td align="center" style="padding:
|
|
1322
|
+
<td align="center" style="padding:0;">
|
|
1323
1323
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
|
|
1324
1324
|
<tr>
|
|
1325
1325
|
<td align="center" style="background-color:#0065b3; padding:22px 36px;">
|
|
@@ -1379,7 +1379,7 @@
|
|
|
1379
1379
|
<body style="margin:0; padding:0; background-color:#10141c;">
|
|
1380
1380
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#10141c;">
|
|
1381
1381
|
<tr>
|
|
1382
|
-
<td align="center" style="padding:
|
|
1382
|
+
<td align="center" style="padding:0;">
|
|
1383
1383
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#1a1f2b; ">
|
|
1384
1384
|
<tr>
|
|
1385
1385
|
<td align="center" style="padding:36px 36px 0;">
|
|
@@ -1519,9 +1519,11 @@
|
|
|
1519
1519
|
|
|
1520
1520
|
// ---- storage ------------------------------------------------------------
|
|
1521
1521
|
//
|
|
1522
|
-
// What may be uploaded is the host app's call, so
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1522
|
+
// What may be uploaded is the host app's call, so sizes ship undefaulted:
|
|
1523
|
+
// with a provider wired and no `maxBytes`, uploads are refused. Formats are
|
|
1524
|
+
// the exception -- every image type is allowed unless `accept` narrows it,
|
|
1525
|
+
// which is why no list appears below. Add one when your audience's mail
|
|
1526
|
+
// clients demand it --
|
|
1525
1527
|
//
|
|
1526
1528
|
// * JPEG/PNG/GIF render everywhere. WebP does not: Outlook on Windows
|
|
1527
1529
|
// draws mail with the Word engine and shows nothing at all. AVIF is
|
|
@@ -1531,11 +1533,11 @@
|
|
|
1531
1533
|
// * Email bodies are 600px wide, so 1600px leaves room for a retina
|
|
1532
1534
|
// asset without letting a 6000px camera original through.
|
|
1533
1535
|
//
|
|
1534
|
-
// SVG is
|
|
1535
|
-
// editor's own DOM as soon as it appears as a library tile --
|
|
1536
|
-
// `accept` is not enough on its own, it also needs
|
|
1536
|
+
// SVG is excluded from the default. It is a script host, and it renders
|
|
1537
|
+
// inside the editor's own DOM as soon as it appears as a library tile --
|
|
1538
|
+
// listing it in `accept` is not enough on its own, it also needs
|
|
1539
|
+
// `allowSvg: true`.
|
|
1537
1540
|
editor.storageLimits = {
|
|
1538
|
-
accept: ['image/jpeg', 'image/png', 'image/gif'],
|
|
1539
1541
|
maxBytes: 2 * 1024 * 1024,
|
|
1540
1542
|
maxWidth: 1600,
|
|
1541
1543
|
maxHeight: 1600,
|
|
@@ -1548,32 +1550,244 @@
|
|
|
1548
1550
|
// calls below at S3, a DMS, a CDN or your own API and the editor is
|
|
1549
1551
|
// unchanged.
|
|
1550
1552
|
//
|
|
1551
|
-
//
|
|
1553
|
+
// This demo wires a real one. There is no server behind a static page, so
|
|
1554
|
+
// the "backend" is IndexedDB in your own browser — but everything the editor
|
|
1555
|
+
// is allowed to see about it behaves like a service: round-trip latency,
|
|
1556
|
+
// folders and search resolved on the far side, cursor-paged listings,
|
|
1557
|
+
// uploads that are still there after a reload, deletes that stay deleted,
|
|
1558
|
+
// and cancellation honoured. Replace `demoBackend` with `fetch` calls to
|
|
1559
|
+
// your API and the provider underneath it does not change a line.
|
|
1552
1560
|
//
|
|
1553
|
-
// editor
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1558
|
-
//
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1561
|
+
// The editor ships no files of its own: with nothing wired here the library
|
|
1562
|
+
// opens empty and stays that way until someone drops something in. Every
|
|
1563
|
+
// file in the Assets modal is defined below — host content, in host code,
|
|
1564
|
+
// exactly where a real integration keeps it.
|
|
1565
|
+
|
|
1566
|
+
// ---- the pretend backend ------------------------------------------------
|
|
1567
|
+
|
|
1568
|
+
const demoBackend = (() => {
|
|
1569
|
+
const DB_NAME = 'mailcraft-demo-assets';
|
|
1570
|
+
const STORE = 'files';
|
|
1571
|
+
const PAGE_SIZE = 8; // small on purpose: it makes "Load more" real
|
|
1572
|
+
const LATENCY = 240; // roughly what one round trip to a DMS costs
|
|
1573
|
+
|
|
1574
|
+
// Folders are the backend's, not the editor's. Ids are what travel over
|
|
1575
|
+
// the wire; the names are only ever display strings.
|
|
1576
|
+
const FOLDERS = [
|
|
1577
|
+
{ id: 'brand', name: 'Brand' },
|
|
1578
|
+
{ id: 'product', name: 'Product' },
|
|
1579
|
+
{ id: 'photography', name: 'Photography' },
|
|
1580
|
+
{ id: 'uploads', name: 'Uploads' },
|
|
1581
|
+
];
|
|
1582
|
+
const folderName = (id) => (FOLDERS.find((f) => f.id === id) || {}).name || '';
|
|
1583
|
+
|
|
1584
|
+
// The demo's own placeholder images, as SVG data URIs so the catalogue
|
|
1585
|
+
// needs no binaries next to this file. A real backend hands back durable
|
|
1586
|
+
// https URLs instead — see the note on `url` at the bottom.
|
|
1587
|
+
const ph = (label, w, ht) => 'data:image/svg+xml;utf8,' + encodeURIComponent(
|
|
1588
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">'
|
|
1589
|
+
+ '<defs><pattern id="p" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">'
|
|
1590
|
+
+ '<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>'
|
|
1591
|
+
+ '<rect width="100%" height="100%" fill="url(#p)"/>'
|
|
1592
|
+
+ '<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>'
|
|
1593
|
+
+ '<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="'
|
|
1594
|
+
+ Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
|
|
1595
|
+
).replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
1596
|
+
|
|
1597
|
+
// Fourteen files across three folders: enough that a listing pages, and
|
|
1598
|
+
// enough that a search narrows something.
|
|
1599
|
+
const CATALOGUE = [
|
|
1600
|
+
['logo-lockup.png', 'brand', 480, 180],
|
|
1601
|
+
['logo-mark.png', 'brand', 240, 240],
|
|
1602
|
+
['logo-wordmark.png', 'brand', 520, 140],
|
|
1603
|
+
['jacket-03-front.jpg', 'product', 520, 360],
|
|
1604
|
+
['jacket-03-detail.jpg', 'product', 520, 360],
|
|
1605
|
+
['jacket-03-lining.jpg', 'product', 520, 360],
|
|
1606
|
+
['boots-pair.jpg', 'product', 520, 360],
|
|
1607
|
+
['boots-sole.jpg', 'product', 520, 360],
|
|
1608
|
+
['satchel-tan.jpg', 'product', 520, 360],
|
|
1609
|
+
['hero-workshop.jpg', 'photography', 600, 340],
|
|
1610
|
+
['hero-storefront.jpg', 'photography', 600, 340],
|
|
1611
|
+
['texture-waxed.jpg', 'photography', 600, 240],
|
|
1612
|
+
['texture-canvas.jpg', 'photography', 600, 240],
|
|
1613
|
+
['team-bench.jpg', 'photography', 600, 400],
|
|
1614
|
+
];
|
|
1615
|
+
|
|
1616
|
+
// IndexedDB is unavailable on a `file://` page in Chrome, and this file is
|
|
1617
|
+
// meant to survive being double-clicked. The in-memory shim keeps every
|
|
1618
|
+
// path below identical; the only thing lost is "survives a reload".
|
|
1619
|
+
let memory = null;
|
|
1620
|
+
let dbPromise = null;
|
|
1621
|
+
function db() {
|
|
1622
|
+
if (memory) return Promise.resolve(null);
|
|
1623
|
+
if (!dbPromise) {
|
|
1624
|
+
dbPromise = new Promise((resolve) => {
|
|
1625
|
+
let req;
|
|
1626
|
+
try { req = indexedDB.open(DB_NAME, 1); } catch (e) { memory = new Map(); return resolve(null); }
|
|
1627
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE, { keyPath: 'id' });
|
|
1628
|
+
req.onsuccess = () => resolve(req.result);
|
|
1629
|
+
req.onerror = () => { memory = new Map(); resolve(null); };
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
return dbPromise;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
async function readAll() {
|
|
1636
|
+
const conn = await db();
|
|
1637
|
+
if (!conn) return Array.from(memory.values());
|
|
1638
|
+
return new Promise((resolve, reject) => {
|
|
1639
|
+
const req = conn.transaction(STORE, 'readonly').objectStore(STORE).getAll();
|
|
1640
|
+
req.onsuccess = () => resolve(req.result || []);
|
|
1641
|
+
req.onerror = () => reject(req.error);
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
async function write(rec) {
|
|
1646
|
+
const conn = await db();
|
|
1647
|
+
if (!conn) { memory.set(rec.id, rec); return rec; }
|
|
1648
|
+
return new Promise((resolve, reject) => {
|
|
1649
|
+
const t = conn.transaction(STORE, 'readwrite');
|
|
1650
|
+
t.objectStore(STORE).put(rec);
|
|
1651
|
+
t.oncomplete = () => resolve(rec);
|
|
1652
|
+
t.onerror = () => reject(t.error);
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
async function drop(id) {
|
|
1657
|
+
const conn = await db();
|
|
1658
|
+
if (!conn) { memory.delete(id); return; }
|
|
1659
|
+
return new Promise((resolve, reject) => {
|
|
1660
|
+
const t = conn.transaction(STORE, 'readwrite');
|
|
1661
|
+
t.objectStore(STORE).delete(id);
|
|
1662
|
+
t.oncomplete = () => resolve();
|
|
1663
|
+
t.onerror = () => reject(t.error);
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// First run only: a real backend already has the customer's files in it.
|
|
1668
|
+
let seeded = null;
|
|
1669
|
+
function seed() {
|
|
1670
|
+
if (!seeded) {
|
|
1671
|
+
seeded = readAll().then(async (rows) => {
|
|
1672
|
+
if (rows.length) return;
|
|
1673
|
+
const t0 = Date.now() - CATALOGUE.length * 1000;
|
|
1674
|
+
for (let i = 0; i < CATALOGUE.length; i++) {
|
|
1675
|
+
const name = CATALOGUE[i][0];
|
|
1676
|
+
const folderId = CATALOGUE[i][1];
|
|
1677
|
+
const w = CATALOGUE[i][2];
|
|
1678
|
+
const ht = CATALOGUE[i][3];
|
|
1679
|
+
const url = ph(name.replace(/\.[a-z]+$/i, '').replace(/-/g, ' '), w, ht);
|
|
1680
|
+
await write({ id: 'seed-' + i, name: name, folderId: folderId, url: url, w: w, ht: ht, size: url.length, createdAt: t0 + i * 1000 });
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
return seeded;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/** Latency you can cancel: an aborted request rejects the way `fetch` does. */
|
|
1688
|
+
function nap(ms, signal) {
|
|
1689
|
+
return new Promise((resolve, reject) => {
|
|
1690
|
+
if (signal && signal.aborted) return reject(new DOMException('Aborted', 'AbortError'));
|
|
1691
|
+
const id = setTimeout(resolve, ms);
|
|
1692
|
+
if (signal) signal.addEventListener('abort', () => { clearTimeout(id); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const readAsDataUrl = (file) => new Promise((resolve, reject) => {
|
|
1697
|
+
const fr = new FileReader();
|
|
1698
|
+
fr.onload = () => resolve(fr.result);
|
|
1699
|
+
fr.onerror = () => reject(new Error('the file could not be read'));
|
|
1700
|
+
fr.readAsDataURL(file);
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
return {
|
|
1704
|
+
folders: async () => { await nap(LATENCY / 2); return FOLDERS.map((f) => ({ id: f.id, name: f.name })); },
|
|
1705
|
+
|
|
1706
|
+
// Folder, search and paging are all resolved here, on the far side of the
|
|
1707
|
+
// wire — which is exactly why the editor stops filtering locally the
|
|
1708
|
+
// moment a provider is assigned. A cursor is opaque to it: this one is an
|
|
1709
|
+
// offset, a real one is usually a continuation token.
|
|
1710
|
+
async list(q) {
|
|
1711
|
+
await seed();
|
|
1712
|
+
await nap(LATENCY, q.signal);
|
|
1713
|
+
const term = (q.query || '').trim().toLowerCase();
|
|
1714
|
+
const rows = (await readAll())
|
|
1715
|
+
.filter((r) => (!q.folderId || r.folderId === q.folderId) && (!term || r.name.toLowerCase().includes(term)))
|
|
1716
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
1717
|
+
const from = Number(q.cursor) || 0;
|
|
1718
|
+
return {
|
|
1719
|
+
items: rows.slice(from, from + PAGE_SIZE).map((r) => ({
|
|
1720
|
+
id: r.id, name: r.name, url: r.url,
|
|
1721
|
+
folder: folderName(r.folderId), folderId: r.folderId,
|
|
1722
|
+
w: r.w, ht: r.ht, size: r.size,
|
|
1723
|
+
})),
|
|
1724
|
+
cursor: from + PAGE_SIZE < rows.length ? String(from + PAGE_SIZE) : null,
|
|
1725
|
+
};
|
|
1726
|
+
},
|
|
1727
|
+
|
|
1728
|
+
// The editor has already validated the file against `storageLimits` and
|
|
1729
|
+
// measured it, so `width`/`height` arrive filled in. Uploading from "All
|
|
1730
|
+
// files" means no folder was chosen — a backend still has to put the
|
|
1731
|
+
// bytes somewhere, and "Uploads" is this one's answer.
|
|
1732
|
+
async upload(file, opts) {
|
|
1733
|
+
const url = await readAsDataUrl(file);
|
|
1734
|
+
await nap(LATENCY * 2, opts.signal);
|
|
1735
|
+
const target = opts.folderId || 'uploads';
|
|
1736
|
+
const rec = {
|
|
1737
|
+
id: 'up-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7),
|
|
1738
|
+
name: file.name, folderId: target, url: url,
|
|
1739
|
+
w: opts.width || 0, ht: opts.height || 0, size: file.size, createdAt: Date.now(),
|
|
1740
|
+
};
|
|
1741
|
+
await write(rec);
|
|
1742
|
+
return { id: rec.id, name: rec.name, url: rec.url, folder: folderName(target), folderId: target, w: rec.w, ht: rec.ht, size: rec.size };
|
|
1743
|
+
},
|
|
1744
|
+
|
|
1745
|
+
async remove(asset) {
|
|
1746
|
+
await nap(LATENCY);
|
|
1747
|
+
await drop(asset.id);
|
|
1748
|
+
},
|
|
1749
|
+
|
|
1750
|
+
/** Not part of the contract — a console escape hatch for this demo. */
|
|
1751
|
+
async reset() {
|
|
1752
|
+
const rows = await readAll();
|
|
1753
|
+
for (let i = 0; i < rows.length; i++) await drop(rows[i].id);
|
|
1754
|
+
seeded = null;
|
|
1755
|
+
await seed();
|
|
1756
|
+
},
|
|
1757
|
+
};
|
|
1758
|
+
})();
|
|
1759
|
+
|
|
1760
|
+
// The provider itself: four thin methods over whatever the backend is. This
|
|
1761
|
+
// is the entire integration — no editor internals, no imports, no build step.
|
|
1762
|
+
const demoStorage = {
|
|
1763
|
+
folders: () => demoBackend.folders(),
|
|
1764
|
+
list: (q) => demoBackend.list(q),
|
|
1765
|
+
upload: (file, opts) => demoBackend.upload(file, opts),
|
|
1766
|
+
remove: (asset) => demoBackend.remove(asset),
|
|
1767
|
+
};
|
|
1768
|
+
editor.storageProvider = demoStorage;
|
|
1769
|
+
|
|
1770
|
+
// Handy while poking at the demo: `mailcraftDemo.reset()` empties the
|
|
1771
|
+
// library, re-seeds it, and re-lists it. Re-assigning the provider is how a
|
|
1772
|
+
// host tells the editor to start over — the same call the line above makes.
|
|
1773
|
+
window.mailcraftDemo = {
|
|
1774
|
+
reset: () => demoBackend.reset().then(() => { editor.storageProvider = demoStorage; }),
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
// Against a real API the same four methods are usually one-liners:
|
|
1778
|
+
//
|
|
1779
|
+
// list: ({ folderId, cursor, query, signal }) => myApi.listImages({ folder: folderId, cursor, q: query, signal })
|
|
1780
|
+
// upload: (file, { folderId, width, height, signal }) => myApi.upload(file, folderId, { signal })
|
|
1781
|
+
// folders: () => myApi.folders(), // optional
|
|
1782
|
+
// remove: (asset) => myApi.delete(asset.id), // optional
|
|
1783
|
+
//
|
|
1784
|
+
// — each response mapped onto `{ id, name, url, folder, folderId, w, ht, size }`.
|
|
1573
1785
|
//
|
|
1574
1786
|
// The one hard requirement is on `url`: it has to still resolve after the
|
|
1575
1787
|
// campaign is sent. A URL that expires in an hour produces mail whose images
|
|
1576
|
-
// are already broken when it lands.
|
|
1788
|
+
// are already broken when it lands. This demo cheats it with data URIs, which
|
|
1789
|
+
// no email client will render — a real integration returns the durable https
|
|
1790
|
+
// URL its CDN serves.
|
|
1577
1791
|
//
|
|
1578
1792
|
// README.md has the full contract.
|
|
1579
1793
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seliseblocks/mailcraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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');
|