@mjml-agent-editor/editor 0.2.0 → 0.3.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/README.md +32 -1
- package/dist/canvas.cjs +445 -83
- package/dist/canvas.cjs.map +1 -1
- package/dist/canvas.d.cts +30 -2
- package/dist/canvas.d.ts +30 -2
- package/dist/canvas.js +437 -79
- package/dist/canvas.js.map +1 -1
- package/dist/{chunk-FTEJNUJL.js → chunk-K7KQC7LX.js} +5 -2
- package/dist/chunk-K7KQC7LX.js.map +1 -0
- package/dist/index.cjs +4 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/editor/editor-theme.css +4 -0
- package/dist/chunk-FTEJNUJL.js.map +0 -1
package/dist/canvas.cjs
CHANGED
|
@@ -31,16 +31,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// src/canvas.ts
|
|
32
32
|
var canvas_exports = {};
|
|
33
33
|
__export(canvas_exports, {
|
|
34
|
-
EmailEditor: () => EmailEditor
|
|
34
|
+
EmailEditor: () => EmailEditor,
|
|
35
|
+
blockProblems: () => blockProblems
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(canvas_exports);
|
|
37
38
|
|
|
38
39
|
// src/components/editor/EmailEditor.tsx
|
|
39
|
-
var
|
|
40
|
+
var import_grapesjs2 = __toESM(require("grapesjs"), 1);
|
|
40
41
|
var import_react9 = __toESM(require("@grapesjs/react"), 1);
|
|
41
|
-
var
|
|
42
|
+
var import_grapesjs_mjml2 = __toESM(require("grapesjs-mjml"), 1);
|
|
42
43
|
var import_react10 = require("react");
|
|
43
|
-
var
|
|
44
|
+
var import_core4 = require("@mjml-agent-editor/core");
|
|
44
45
|
|
|
45
46
|
// src/stores.tsx
|
|
46
47
|
var import_react = require("react");
|
|
@@ -68,8 +69,10 @@ var DEFAULT_LABELS = {
|
|
|
68
69
|
tabLayers: "Layers",
|
|
69
70
|
searchBlocks: "Search blocks\u2026",
|
|
70
71
|
noBlocksMatch: "No blocks match.",
|
|
72
|
+
blockAdded: (block) => `Added: ${block}`,
|
|
71
73
|
blockCategoryLayout: "Layout",
|
|
72
74
|
blockCategoryContent: "Content",
|
|
75
|
+
blockCategorySections: "Sections",
|
|
73
76
|
blockLabels: {
|
|
74
77
|
"mj-1-column": "1 column",
|
|
75
78
|
"mj-2-columns": "2 columns",
|
|
@@ -97,7 +100,8 @@ var DEFAULT_LABELS = {
|
|
|
97
100
|
"mj-navbar-link": "Navigation link",
|
|
98
101
|
"mj-social": "Social links",
|
|
99
102
|
"mj-social-element": "Social link",
|
|
100
|
-
"mj-raw": "Raw HTML"
|
|
103
|
+
"mj-raw": "Raw HTML",
|
|
104
|
+
"mj-table": "Table"
|
|
101
105
|
},
|
|
102
106
|
expandAll: "Expand all",
|
|
103
107
|
selectAnElement: "Select an element\u2026",
|
|
@@ -937,6 +941,30 @@ function CanvasComments({
|
|
|
937
941
|
] });
|
|
938
942
|
}
|
|
939
943
|
|
|
944
|
+
// src/lib/save-baseline.ts
|
|
945
|
+
function createSaveBaseline() {
|
|
946
|
+
let stored = "";
|
|
947
|
+
let loaded = null;
|
|
948
|
+
return {
|
|
949
|
+
/** The MJML the store holds, as far as this editor knows. */
|
|
950
|
+
get stored() {
|
|
951
|
+
return stored;
|
|
952
|
+
},
|
|
953
|
+
/** `canvas` is what the load serialises to, or null when it repaired something to write. */
|
|
954
|
+
load(mjml, canvas) {
|
|
955
|
+
stored = mjml;
|
|
956
|
+
loaded = canvas;
|
|
957
|
+
},
|
|
958
|
+
saved(mjml) {
|
|
959
|
+
stored = mjml;
|
|
960
|
+
loaded = null;
|
|
961
|
+
},
|
|
962
|
+
changed(mjml) {
|
|
963
|
+
return mjml !== stored && mjml !== loaded;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
940
968
|
// src/lib/save-queue.ts
|
|
941
969
|
function createSaveQueue(save, delayMs) {
|
|
942
970
|
let timer = null;
|
|
@@ -966,6 +994,39 @@ function createSaveQueue(save, delayMs) {
|
|
|
966
994
|
};
|
|
967
995
|
}
|
|
968
996
|
|
|
997
|
+
// src/lib/turn-follower.ts
|
|
998
|
+
function createTurnFollower() {
|
|
999
|
+
let running = false;
|
|
1000
|
+
let mounted = true;
|
|
1001
|
+
let apply = null;
|
|
1002
|
+
return {
|
|
1003
|
+
turn(on) {
|
|
1004
|
+
if (on === running) return;
|
|
1005
|
+
running = on;
|
|
1006
|
+
if (mounted) apply?.(on);
|
|
1007
|
+
},
|
|
1008
|
+
/**
|
|
1009
|
+
* Whether the canvas is still mounted. An unmounted one's late reload must not unlock
|
|
1010
|
+
* the shared state a canvas mounted since has locked.
|
|
1011
|
+
*/
|
|
1012
|
+
get mounted() {
|
|
1013
|
+
return mounted;
|
|
1014
|
+
},
|
|
1015
|
+
// Both, as React mounts twice in development.
|
|
1016
|
+
mount() {
|
|
1017
|
+
mounted = true;
|
|
1018
|
+
},
|
|
1019
|
+
unmount() {
|
|
1020
|
+
mounted = false;
|
|
1021
|
+
},
|
|
1022
|
+
/** A turn already under way is applied at once; none means nothing to apply. */
|
|
1023
|
+
ready(fn) {
|
|
1024
|
+
apply = fn;
|
|
1025
|
+
if (running) fn(true);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
969
1030
|
// src/lib/shortcuts.ts
|
|
970
1031
|
function opensBlockSearch(e) {
|
|
971
1032
|
if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey || e.isComposing) return false;
|
|
@@ -1282,6 +1343,7 @@ var import_react5 = require("react");
|
|
|
1282
1343
|
var import_lucide_react5 = require("lucide-react");
|
|
1283
1344
|
|
|
1284
1345
|
// src/components/editor/inspector/values.ts
|
|
1346
|
+
var import_core2 = require("@mjml-agent-editor/core");
|
|
1285
1347
|
var SIDES = ["top", "right", "bottom", "left"];
|
|
1286
1348
|
function expandBox(value) {
|
|
1287
1349
|
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1312,15 +1374,6 @@ function parseNetwork(name) {
|
|
|
1312
1374
|
function networkName(network, share) {
|
|
1313
1375
|
return share || network === "custom" ? network : `${network}${NO_SHARE}`;
|
|
1314
1376
|
}
|
|
1315
|
-
var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel", "sms"]);
|
|
1316
|
-
function isSafeLink(value) {
|
|
1317
|
-
const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/:/g, ":").replace(/&(Tab|NewLine);/g, "").replace(/[\u0000-\u0020]/g, "");
|
|
1318
|
-
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(decoded)?.[1];
|
|
1319
|
-
return !scheme || LINK_SCHEMES.has(scheme.toLowerCase());
|
|
1320
|
-
}
|
|
1321
|
-
function isSafeNavbarLink(baseUrl, href) {
|
|
1322
|
-
return isSafeLink(baseUrl) && isSafeLink(href) && isSafeLink(baseUrl + href);
|
|
1323
|
-
}
|
|
1324
1377
|
|
|
1325
1378
|
// src/components/editor/inspector/controls.tsx
|
|
1326
1379
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
@@ -2126,6 +2179,23 @@ function withoutEditorProps(mjml) {
|
|
|
2126
2179
|
)
|
|
2127
2180
|
);
|
|
2128
2181
|
}
|
|
2182
|
+
function withClosedTags(mjml) {
|
|
2183
|
+
return mjml.replace(START_TAG, (tag) => {
|
|
2184
|
+
const selfClosed = /^<(mj-[\w-]+)([\s\S]*?)\s*\/>$/i.exec(tag);
|
|
2185
|
+
return selfClosed ? `<${selfClosed[1]}${selfClosed[2]}></${selfClosed[1]}>` : tag;
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
var TABLE = /<!--[\s\S]*?(?:-->|$)|(<mj-table\b(?:[^>"']|"[^"]*"|'[^']*')*>)((?:<!--[\s\S]*?-->|(?!<!--)[\s\S])*?)(<\/mj-table\s*>)/gi;
|
|
2189
|
+
function withTableRowsKept(mjml) {
|
|
2190
|
+
return mjml.replace(
|
|
2191
|
+
TABLE,
|
|
2192
|
+
(match, open, rows, close) => open ? `${open}<template>${rows}</template>${close}` : match
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
function sourceForEditor(mjml) {
|
|
2196
|
+
const cleaned = withoutEditorProps(mjml);
|
|
2197
|
+
return { mjml: withTableRowsKept(withClosedTags(cleaned)), repaired: cleaned !== mjml };
|
|
2198
|
+
}
|
|
2129
2199
|
function illegalAttributes(tag, attributes) {
|
|
2130
2200
|
const allowed = ALLOWED_ATTRIBUTES[tag];
|
|
2131
2201
|
if (!allowed) return [];
|
|
@@ -3002,7 +3072,10 @@ var ICONS = {
|
|
|
3002
3072
|
"mj-navbar-link": svg(
|
|
3003
3073
|
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'
|
|
3004
3074
|
),
|
|
3005
|
-
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>')
|
|
3075
|
+
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>'),
|
|
3076
|
+
"mj-table": svg(
|
|
3077
|
+
'<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/>'
|
|
3078
|
+
)
|
|
3006
3079
|
};
|
|
3007
3080
|
function tagOf(c) {
|
|
3008
3081
|
return String(c.get("tagName") || c.get("type") || "");
|
|
@@ -3465,12 +3538,12 @@ function ElementSettings({
|
|
|
3465
3538
|
};
|
|
3466
3539
|
const safeLink = (attr) => (link) => {
|
|
3467
3540
|
if (tag === "mj-navbar-link" && attr === "href") {
|
|
3468
|
-
return isSafeNavbarLink(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
|
|
3541
|
+
return (0, import_core2.isSafeNavbarLink)(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
|
|
3469
3542
|
}
|
|
3470
3543
|
if (tag === "mj-navbar" && attr === "base-url") {
|
|
3471
|
-
return component.components().every((c) => isSafeNavbarLink(link, String(c.getAttributes().href ?? "")));
|
|
3544
|
+
return component.components().every((c) => (0, import_core2.isSafeNavbarLink)(link, String(c.getAttributes().href ?? "")));
|
|
3472
3545
|
}
|
|
3473
|
-
return isSafeLink(link);
|
|
3546
|
+
return (0, import_core2.isSafeLink)(link);
|
|
3474
3547
|
};
|
|
3475
3548
|
const control = (f) => {
|
|
3476
3549
|
const value = attributes[f.attr] ?? "";
|
|
@@ -3778,8 +3851,15 @@ function Header({
|
|
|
3778
3851
|
] });
|
|
3779
3852
|
}
|
|
3780
3853
|
|
|
3854
|
+
// src/components/editor/blocks.ts
|
|
3855
|
+
var import_grapesjs = __toESM(require("grapesjs"), 1);
|
|
3856
|
+
var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
|
|
3857
|
+
|
|
3781
3858
|
// src/components/editor/ids.ts
|
|
3782
|
-
var ID_RE = {
|
|
3859
|
+
var ID_RE = {
|
|
3860
|
+
sec: /(?:^|\s)sec-([A-Za-z0-9_-]+)(?=\s|$)/,
|
|
3861
|
+
obj: /(?:^|\s)obj-([A-Za-z0-9_-]+)(?=\s|$)/
|
|
3862
|
+
};
|
|
3783
3863
|
function idOf(c, prefix) {
|
|
3784
3864
|
return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
|
|
3785
3865
|
}
|
|
@@ -3808,19 +3888,292 @@ function freshenCopiedIds(root, added) {
|
|
|
3808
3888
|
const id = idOf(c, prefix);
|
|
3809
3889
|
if (!id || !used(prefix, id, c)) continue;
|
|
3810
3890
|
const cssClass = String(c.getAttributes()["css-class"]);
|
|
3811
|
-
|
|
3891
|
+
const fresh = cssClass.replace(
|
|
3892
|
+
ID_RE[prefix],
|
|
3893
|
+
(token) => token.replace(`${prefix}-${id}`, `${prefix}-${newId()}`)
|
|
3894
|
+
);
|
|
3895
|
+
c.addAttributes({ "css-class": fresh });
|
|
3896
|
+
}
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// src/components/editor/insert-block.ts
|
|
3901
|
+
var addsBlock = (e) => (e.key === "Enter" || e.key === " ") && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
3902
|
+
var tileAttributes = (id) => ({ tabindex: "0", role: "button", "data-block": id });
|
|
3903
|
+
var tagOf5 = (c) => String(c.get("tagName") ?? "");
|
|
3904
|
+
function closest(c, tag) {
|
|
3905
|
+
for (let cur = c; cur; cur = cur.parent() ?? void 0) if (tagOf5(cur) === tag) return cur;
|
|
3906
|
+
return void 0;
|
|
3907
|
+
}
|
|
3908
|
+
function childOf(parent, c) {
|
|
3909
|
+
let cur = c;
|
|
3910
|
+
while (cur.parent() && cur.parent() !== parent) cur = cur.parent();
|
|
3911
|
+
return cur;
|
|
3912
|
+
}
|
|
3913
|
+
function insertBlock(editor, block) {
|
|
3914
|
+
const content = block.getContent();
|
|
3915
|
+
let body;
|
|
3916
|
+
editor.getWrapper()?.onAll((c) => {
|
|
3917
|
+
if (!body && tagOf5(c) === "mj-body") body = c;
|
|
3918
|
+
});
|
|
3919
|
+
if (typeof content !== "string" || !body) return void 0;
|
|
3920
|
+
const selected = editor.getSelected();
|
|
3921
|
+
const section = closest(selected, "mj-section");
|
|
3922
|
+
const sections = section?.parent() ?? body;
|
|
3923
|
+
const afterSection = section ? { at: section.index() + 1 } : {};
|
|
3924
|
+
const column = closest(selected, "mj-column");
|
|
3925
|
+
let added;
|
|
3926
|
+
if (/^\s*<mj-section\b/i.test(content)) {
|
|
3927
|
+
added = sections.append(content, afterSection)[0];
|
|
3928
|
+
} else if (column && selected) {
|
|
3929
|
+
const at = selected === column ? {} : { at: childOf(column, selected).index() + 1 };
|
|
3930
|
+
added = column.append(content, at)[0];
|
|
3931
|
+
} else {
|
|
3932
|
+
const [wrapper] = sections.append(
|
|
3933
|
+
`<mj-section><mj-column>${content}</mj-column></mj-section>`,
|
|
3934
|
+
afterSection
|
|
3935
|
+
);
|
|
3936
|
+
added = wrapper?.components().at(0)?.components().at(0);
|
|
3937
|
+
}
|
|
3938
|
+
if (!added) return void 0;
|
|
3939
|
+
editor.select(added);
|
|
3940
|
+
if (block.get("activate")) added.getView()?.onActive(void 0);
|
|
3941
|
+
return added;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
// src/components/editor/blocks.ts
|
|
3945
|
+
var EDITABLE_TAGS = /* @__PURE__ */ new Set([
|
|
3946
|
+
"mj-section",
|
|
3947
|
+
"mj-group",
|
|
3948
|
+
"mj-column",
|
|
3949
|
+
"mj-text",
|
|
3950
|
+
"mj-image",
|
|
3951
|
+
"mj-button",
|
|
3952
|
+
"mj-divider",
|
|
3953
|
+
"mj-spacer",
|
|
3954
|
+
"mj-social",
|
|
3955
|
+
"mj-social-element",
|
|
3956
|
+
"mj-navbar",
|
|
3957
|
+
"mj-navbar-link"
|
|
3958
|
+
]);
|
|
3959
|
+
var CONTENT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-social-element", "mj-navbar-link"]);
|
|
3960
|
+
var EDITOR_INTERNALS = /data-gjs-|__p\b|custom-name/i;
|
|
3961
|
+
var DROPPED_MARKUP = /<(?:script|style|link|meta|iframe|object|embed|form)\b/i;
|
|
3962
|
+
var INLINE_IMAGE = /\b(?:src|srcset|background-url|icon)\s*=\s*(?:"[^"]*data:|'[^']*data:|data:)/i;
|
|
3963
|
+
var LINK_IN_MARKUP = /[\s/"'](?:xlink:)?(?:href|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
3964
|
+
var validator;
|
|
3965
|
+
function blockProblems(mjml, editor = validator ??= import_grapesjs.default.init({
|
|
3966
|
+
headless: true,
|
|
3967
|
+
storageManager: false,
|
|
3968
|
+
telemetry: false,
|
|
3969
|
+
plugins: [import_grapesjs_mjml.default]
|
|
3970
|
+
})) {
|
|
3971
|
+
if (EDITOR_INTERNALS.test(mjml)) return ["it contains GrapesJS's own attributes"];
|
|
3972
|
+
if (INLINE_IMAGE.test(mjml)) return ["it inlines an image as a data: URL"];
|
|
3973
|
+
let tree;
|
|
3974
|
+
try {
|
|
3975
|
+
tree = editor.runCommand("mjml-code-to-html", {
|
|
3976
|
+
mjml: `<mjml><mj-body>${withClosedTags(mjml)}</mj-body></mjml>`,
|
|
3977
|
+
validationLevel: "strict"
|
|
3978
|
+
}).json;
|
|
3979
|
+
} catch (e) {
|
|
3980
|
+
const errors = e.errors;
|
|
3981
|
+
if (!errors) throw e;
|
|
3982
|
+
return errors.map((error) => error.formattedMessage ?? error.message ?? String(error));
|
|
3983
|
+
}
|
|
3984
|
+
const problems = [];
|
|
3985
|
+
const top = tree.children?.find((c) => c.tagName === "mj-body")?.children ?? [];
|
|
3986
|
+
if (top.length !== 1 || top[0].tagName !== "mj-section") {
|
|
3987
|
+
problems.push("it is not exactly one mj-section");
|
|
3988
|
+
}
|
|
3989
|
+
const visit = (node, parent) => {
|
|
3990
|
+
const tag = node.tagName;
|
|
3991
|
+
const attributes = node.attributes ?? {};
|
|
3992
|
+
const content = node.content ?? "";
|
|
3993
|
+
if (!EDITABLE_TAGS.has(tag)) problems.push(`${tag} is not editable here`);
|
|
3994
|
+
const cssClass = attributes["css-class"] ?? "";
|
|
3995
|
+
if (ID_RE.sec.test(cssClass) || ID_RE.obj.test(cssClass)) {
|
|
3996
|
+
problems.push(`${tag} carries a sec-* or obj-* id`);
|
|
3997
|
+
}
|
|
3998
|
+
if ("mj-class" in attributes) problems.push(`${tag} uses mj-class`);
|
|
3999
|
+
const links = [attributes.href ?? ""];
|
|
4000
|
+
for (const match of content.matchAll(LINK_IN_MARKUP)) {
|
|
4001
|
+
links.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
4002
|
+
}
|
|
4003
|
+
const linked = tag === "mj-navbar-link" ? (0, import_core2.isSafeNavbarLink)(parent?.attributes?.["base-url"] ?? "", attributes.href ?? "") : true;
|
|
4004
|
+
if (!linked || !links.every(import_core2.isSafeLink)) {
|
|
4005
|
+
problems.push(`${tag} has a link scheme mail must not carry`);
|
|
4006
|
+
}
|
|
4007
|
+
if (CONTENT_TAGS.has(tag) ? /<mj-/i.test(content) || DROPPED_MARKUP.test(content) : content.trim()) {
|
|
4008
|
+
problems.push(`${tag} holds content the editor does not keep`);
|
|
4009
|
+
}
|
|
4010
|
+
node.children?.forEach((child) => visit(child, node));
|
|
4011
|
+
};
|
|
4012
|
+
top.forEach((node) => visit(node));
|
|
4013
|
+
return problems;
|
|
4014
|
+
}
|
|
4015
|
+
var escapeHtml3 = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4016
|
+
function registerBlocks(editor, blocks, category, media) {
|
|
4017
|
+
for (const block of blocks) {
|
|
4018
|
+
const problems = editor.Blocks.get(block.id) ? [`the id is already taken`] : blockProblems(block.mjml, editor);
|
|
4019
|
+
if (problems.length > 0) {
|
|
4020
|
+
console.error(`Block "${block.id}" is left out of the palette: ${problems.join("; ")}`);
|
|
4021
|
+
continue;
|
|
4022
|
+
}
|
|
4023
|
+
editor.Blocks.add(block.id, {
|
|
4024
|
+
// GrapesJS writes the label into the tile as markup.
|
|
4025
|
+
label: escapeHtml3(block.label),
|
|
4026
|
+
category,
|
|
4027
|
+
media: block.media ?? media,
|
|
4028
|
+
content: withClosedTags(block.mjml),
|
|
4029
|
+
attributes: tileAttributes(block.id)
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
// src/components/editor/builtin-blocks.ts
|
|
4035
|
+
var BUILTIN_BLOCKS = [
|
|
4036
|
+
"mj-1-column",
|
|
4037
|
+
"mj-2-columns",
|
|
4038
|
+
"mj-3-columns",
|
|
4039
|
+
"mj-text",
|
|
4040
|
+
"mj-image",
|
|
4041
|
+
"mj-button",
|
|
4042
|
+
"mj-divider",
|
|
4043
|
+
"mj-spacer",
|
|
4044
|
+
"mj-social-group",
|
|
4045
|
+
"mj-navbar"
|
|
4046
|
+
];
|
|
4047
|
+
var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
|
|
4048
|
+
var CONTENT2 = {
|
|
4049
|
+
// The plugin's version links Google+ and shares the page: a bare network name makes the
|
|
4050
|
+
// icon open a "share this link" dialog, while `-noshare` links to the profile a footer
|
|
4051
|
+
// means. Without `alt` each link is an unnamed image to a screen reader.
|
|
4052
|
+
"mj-social-group": `<mj-social icon-size="24px" mode="horizontal">
|
|
4053
|
+
<mj-social-element name="facebook-noshare" href="https://facebook.com/" alt="Facebook"></mj-social-element>
|
|
4054
|
+
<mj-social-element name="instagram-noshare" href="https://instagram.com/" alt="Instagram"></mj-social-element>
|
|
4055
|
+
<mj-social-element name="x-noshare" href="https://x.com/" alt="X"></mj-social-element>
|
|
4056
|
+
</mj-social>`,
|
|
4057
|
+
// The plugin's sample is a via.placeholder.com picture, stored in the email and loaded by
|
|
4058
|
+
// every reader's mail client. Dropping one opens the image picker instead.
|
|
4059
|
+
"mj-image": "<mj-image></mj-image>"
|
|
4060
|
+
};
|
|
4061
|
+
function builtinBlock(labels) {
|
|
4062
|
+
return (id) => ({
|
|
4063
|
+
label: labels.blockLabels[id] ?? id,
|
|
4064
|
+
// By id: a regex on the label broke as soon as a host translated it.
|
|
4065
|
+
category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
|
|
4066
|
+
...id in CONTENT2 && { content: CONTENT2[id] },
|
|
4067
|
+
attributes: tileAttributes(id)
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
var CANVAS_CSS = 'img:not([src]), img[src=""] { min-height: 120px; background: #f4f4f5; outline: 1px dashed #a1a1aa; outline-offset: -1px; }';
|
|
4071
|
+
|
|
4072
|
+
// src/components/editor/table-type.ts
|
|
4073
|
+
var import_core3 = require("@mjml-agent-editor/core");
|
|
4074
|
+
var DROPPED = "script, style, link, iframe, frame, object, embed, applet, base, meta, animate, set, svg, math, template";
|
|
4075
|
+
var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href", "action", "formaction"]);
|
|
4076
|
+
function cleanRows(rows) {
|
|
4077
|
+
rows.querySelectorAll(DROPPED).forEach((el) => el.remove());
|
|
4078
|
+
rows.querySelectorAll("*").forEach((el) => {
|
|
4079
|
+
for (const { name, value } of [...el.attributes]) {
|
|
4080
|
+
const lower = name.toLowerCase();
|
|
4081
|
+
const script = /^(java|vb)script:/i.test(value.replace(/[\u0000-\u0020]/g, ""));
|
|
4082
|
+
if (lower.startsWith("on") || script || LINK_ATTRIBUTES.has(lower) && !(0, import_core3.isSafeLink)(value)) {
|
|
4083
|
+
el.removeAttribute(name);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
4088
|
+
var ROW_STYLE = "pointer-events: all; display: table; width: 100%;";
|
|
4089
|
+
function tablePreview(doc, attributes, rows) {
|
|
4090
|
+
const attr = (name, fallback) => String(attributes[name] ?? fallback);
|
|
4091
|
+
const template = doc.createElement("template");
|
|
4092
|
+
template.innerHTML = `<table>${rows}</table>`;
|
|
4093
|
+
const table = doc.createElement("table");
|
|
4094
|
+
table.setAttribute("cellpadding", String(parseInt(attr("cellpadding", "0"), 10) || 0));
|
|
4095
|
+
table.setAttribute("cellspacing", String(parseInt(attr("cellspacing", "0"), 10) || 0));
|
|
4096
|
+
Object.assign(table.style, {
|
|
4097
|
+
width: attr("width", "100%"),
|
|
4098
|
+
border: attr("border", "none"),
|
|
4099
|
+
color: attr("color", "#000000"),
|
|
4100
|
+
fontFamily: attr("font-family", "Ubuntu, Helvetica, Arial, sans-serif"),
|
|
4101
|
+
fontSize: attr("font-size", "13px"),
|
|
4102
|
+
lineHeight: attr("line-height", "22px"),
|
|
4103
|
+
tableLayout: attr("table-layout", "auto")
|
|
4104
|
+
});
|
|
4105
|
+
for (const row of template.content.querySelectorAll("tr")) {
|
|
4106
|
+
const tr = table.insertRow();
|
|
4107
|
+
for (const cell2 of row.querySelectorAll(":scope > th, :scope > td")) {
|
|
4108
|
+
const copy = doc.createElement(cell2.tagName.toLowerCase());
|
|
4109
|
+
copy.textContent = cell2.textContent;
|
|
4110
|
+
const align = cell2.getAttribute("align");
|
|
4111
|
+
if (align && /^(left|right|center)$/i.test(align)) copy.setAttribute("align", align);
|
|
4112
|
+
tr.append(copy);
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
const cell = doc.createElement("td");
|
|
4116
|
+
cell.style.padding = attr("padding", "10px 25px");
|
|
4117
|
+
cell.append(table);
|
|
4118
|
+
return cell;
|
|
4119
|
+
}
|
|
4120
|
+
function addTableType(editor) {
|
|
4121
|
+
const base = editor.Components.getType("default").view.prototype;
|
|
4122
|
+
editor.Components.addType("mj-table", {
|
|
4123
|
+
isComponent: (el) => {
|
|
4124
|
+
if (el.tagName?.toLowerCase() !== "mj-table") return false;
|
|
4125
|
+
const rows = el.querySelector(":scope > template");
|
|
4126
|
+
if (rows) cleanRows(rows.content);
|
|
4127
|
+
return { type: "mj-table", content: rows?.innerHTML ?? "", components: [] };
|
|
4128
|
+
},
|
|
4129
|
+
model: { defaults: { tagName: "mj-table", droppable: false, editable: false } },
|
|
4130
|
+
view: {
|
|
4131
|
+
tagName: "tr",
|
|
4132
|
+
// GrapesJS clears the element's attributes, style among them, when the model's change.
|
|
4133
|
+
updateAttributes(...args) {
|
|
4134
|
+
base.updateAttributes.apply(this, args);
|
|
4135
|
+
this.el.style.cssText = ROW_STYLE;
|
|
4136
|
+
},
|
|
4137
|
+
updateContent() {
|
|
4138
|
+
const { el, model } = this;
|
|
4139
|
+
el.style.cssText = ROW_STYLE;
|
|
4140
|
+
el.replaceChildren(
|
|
4141
|
+
tablePreview(el.ownerDocument, model.getAttributes(), String(model.get("content") ?? ""))
|
|
4142
|
+
);
|
|
4143
|
+
}
|
|
3812
4144
|
}
|
|
3813
4145
|
});
|
|
3814
4146
|
}
|
|
3815
4147
|
|
|
3816
4148
|
// src/components/editor/plugin-types.ts
|
|
4149
|
+
var COLUMN_CHILDREN = /* @__PURE__ */ new Set([
|
|
4150
|
+
"mj-accordion",
|
|
4151
|
+
"mj-button",
|
|
4152
|
+
"mj-carousel",
|
|
4153
|
+
"mj-divider",
|
|
4154
|
+
"mj-image",
|
|
4155
|
+
"mj-navbar",
|
|
4156
|
+
"mj-raw",
|
|
4157
|
+
"mj-social",
|
|
4158
|
+
"mj-spacer",
|
|
4159
|
+
"mj-table",
|
|
4160
|
+
"mj-text"
|
|
4161
|
+
]);
|
|
3817
4162
|
function fixPluginTypes(editor) {
|
|
4163
|
+
addTableType(editor);
|
|
3818
4164
|
for (const type of editor.Components.getTypes()) {
|
|
3819
|
-
if (!type.id.startsWith("mj-")) continue;
|
|
4165
|
+
if (!type.id.startsWith("mj-") || type.id === "mj-table") continue;
|
|
3820
4166
|
const base = type.model.prototype.getAttrToHTML;
|
|
3821
4167
|
editor.Components.addType(type.id, {
|
|
3822
4168
|
model: {
|
|
3823
|
-
defaults: {
|
|
4169
|
+
defaults: {
|
|
4170
|
+
"style-default": {},
|
|
4171
|
+
...type.id === "mj-column" && {
|
|
4172
|
+
droppable: (source) => COLUMN_CHILDREN.has(String(source.get("tagName")))
|
|
4173
|
+
},
|
|
4174
|
+
// GrapesJS's own is an SVG's markup, which the canvas wrote into `src` and saved.
|
|
4175
|
+
...type.id === "mj-image" && { src: "" }
|
|
4176
|
+
},
|
|
3824
4177
|
getAttrToHTML() {
|
|
3825
4178
|
const attributes = base.call(this);
|
|
3826
4179
|
delete attributes.__p;
|
|
@@ -3842,16 +4195,17 @@ var TYPE_LABEL = {
|
|
|
3842
4195
|
"mj-image": "Image",
|
|
3843
4196
|
"mj-column": "Column"
|
|
3844
4197
|
};
|
|
3845
|
-
function
|
|
4198
|
+
function tagOf6(c) {
|
|
3846
4199
|
return String(c.get("tagName") || c.get("type") || "");
|
|
3847
4200
|
}
|
|
3848
4201
|
var SPARKLES_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
|
|
3849
4202
|
var IMAGE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"/><path d="m14 19.5 3-3 3 3"/><path d="M17 22v-5.5"/><circle cx="9" cy="9" r="2"/></svg>';
|
|
4203
|
+
var SECTION_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="7" x="3" y="3" rx="1"/><rect width="9" height="7" x="3" y="14" rx="1"/><rect width="5" height="7" x="16" y="14" rx="1"/></svg>';
|
|
3850
4204
|
function isSection(c) {
|
|
3851
|
-
return
|
|
4205
|
+
return tagOf6(c) === "mj-section";
|
|
3852
4206
|
}
|
|
3853
4207
|
function isCommentable(c) {
|
|
3854
|
-
const t =
|
|
4208
|
+
const t = tagOf6(c);
|
|
3855
4209
|
return t === "mj-section" || t in TYPE_LABEL;
|
|
3856
4210
|
}
|
|
3857
4211
|
function closestSection(c) {
|
|
@@ -3863,29 +4217,11 @@ function closestSection(c) {
|
|
|
3863
4217
|
return null;
|
|
3864
4218
|
}
|
|
3865
4219
|
function crumbName(c, labels) {
|
|
3866
|
-
const t =
|
|
4220
|
+
const t = tagOf6(c);
|
|
3867
4221
|
return labels.elementNames[t] ?? t.replace(/^mj-/, "");
|
|
3868
4222
|
}
|
|
3869
|
-
var BLOCKS = [
|
|
3870
|
-
"mj-1-column",
|
|
3871
|
-
"mj-2-columns",
|
|
3872
|
-
"mj-3-columns",
|
|
3873
|
-
"mj-text",
|
|
3874
|
-
"mj-image",
|
|
3875
|
-
"mj-button",
|
|
3876
|
-
"mj-divider",
|
|
3877
|
-
"mj-spacer",
|
|
3878
|
-
"mj-social-group",
|
|
3879
|
-
"mj-navbar"
|
|
3880
|
-
];
|
|
3881
|
-
var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
|
|
3882
|
-
var SOCIAL_BLOCK = `<mj-social icon-size="24px" mode="horizontal">
|
|
3883
|
-
<mj-social-element name="facebook-noshare" href="https://facebook.com/"></mj-social-element>
|
|
3884
|
-
<mj-social-element name="instagram-noshare" href="https://instagram.com/"></mj-social-element>
|
|
3885
|
-
<mj-social-element name="x-noshare" href="https://x.com/"></mj-social-element>
|
|
3886
|
-
</mj-social>`;
|
|
3887
4223
|
function objectLabel(c) {
|
|
3888
|
-
const base = TYPE_LABEL[
|
|
4224
|
+
const base = TYPE_LABEL[tagOf6(c)] ?? tagOf6(c);
|
|
3889
4225
|
const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
|
|
3890
4226
|
return text ? `${base}: \u201C${text}\u201D` : base;
|
|
3891
4227
|
}
|
|
@@ -3953,7 +4289,13 @@ function setupRichText(editor, labels) {
|
|
|
3953
4289
|
result: (r, action) => r.exec("hiliteColor", val(action, "input"))
|
|
3954
4290
|
});
|
|
3955
4291
|
}
|
|
3956
|
-
function EmailEditor({
|
|
4292
|
+
function EmailEditor({
|
|
4293
|
+
docId,
|
|
4294
|
+
onReady,
|
|
4295
|
+
commentsRefresh,
|
|
4296
|
+
onOpenCountChange,
|
|
4297
|
+
blocks = []
|
|
4298
|
+
}) {
|
|
3957
4299
|
const documents = useDocumentStore();
|
|
3958
4300
|
const labels = useLabels();
|
|
3959
4301
|
const loadingRef = (0, import_react10.useRef)(false);
|
|
@@ -3975,8 +4317,15 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3975
4317
|
const readOnlyRef = (0, import_react10.useRef)(false);
|
|
3976
4318
|
const applyReadOnly = (0, import_react10.useRef)(async () => {
|
|
3977
4319
|
});
|
|
3978
|
-
(0, import_react10.
|
|
3979
|
-
(0, import_react10.useEffect)(() => ()
|
|
4320
|
+
const [turn] = (0, import_react10.useState)(createTurnFollower);
|
|
4321
|
+
(0, import_react10.useEffect)(() => turn.turn(turnRunning), [turn, turnRunning]);
|
|
4322
|
+
(0, import_react10.useEffect)(() => {
|
|
4323
|
+
turn.mount();
|
|
4324
|
+
return () => {
|
|
4325
|
+
turn.unmount();
|
|
4326
|
+
setLocked(false);
|
|
4327
|
+
};
|
|
4328
|
+
}, [turn, setLocked]);
|
|
3980
4329
|
const [crumbs, setCrumbs] = (0, import_react10.useState)([]);
|
|
3981
4330
|
const [picker, setPicker] = (0, import_react10.useState)(
|
|
3982
4331
|
null
|
|
@@ -3987,7 +4336,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3987
4336
|
const chain = [];
|
|
3988
4337
|
let cur = c;
|
|
3989
4338
|
while (cur) {
|
|
3990
|
-
const t =
|
|
4339
|
+
const t = tagOf6(cur);
|
|
3991
4340
|
if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
|
|
3992
4341
|
chain.unshift(cur);
|
|
3993
4342
|
cur = cur.parent() ?? void 0;
|
|
@@ -4036,7 +4385,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4036
4385
|
if (isSection(c)) addIdClass(c, "sec");
|
|
4037
4386
|
if (!isCommentable(c)) return;
|
|
4038
4387
|
const toolbar = [...c.get("toolbar") ?? []];
|
|
4039
|
-
if (
|
|
4388
|
+
if (tagOf6(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
|
|
4040
4389
|
toolbar.push({
|
|
4041
4390
|
command: "open-image-picker",
|
|
4042
4391
|
label: IMAGE_SVG,
|
|
@@ -4056,7 +4405,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4056
4405
|
editor.Commands.add("open-image-picker", {
|
|
4057
4406
|
run(ed) {
|
|
4058
4407
|
const target = ed.getSelected();
|
|
4059
|
-
if (!target ||
|
|
4408
|
+
if (!target || tagOf6(target) !== "mj-image") return;
|
|
4060
4409
|
const am = ed.AssetManager;
|
|
4061
4410
|
am.open({
|
|
4062
4411
|
target,
|
|
@@ -4076,6 +4425,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4076
4425
|
});
|
|
4077
4426
|
setupRichText(editor, labels);
|
|
4078
4427
|
fixPluginTypes(editor);
|
|
4428
|
+
registerBlocks(editor, blocks, labels.blockCategorySections, SECTION_SVG);
|
|
4079
4429
|
setupLayerNames(editor, labels);
|
|
4080
4430
|
const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
|
|
4081
4431
|
const showBodyInLayers = () => {
|
|
@@ -4083,14 +4433,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4083
4433
|
if (!body) return;
|
|
4084
4434
|
editor.Layers.setRoot(body);
|
|
4085
4435
|
body.onAll((c) => {
|
|
4086
|
-
if (STRUCTURE.has(
|
|
4436
|
+
if (STRUCTURE.has(tagOf6(c))) editor.Layers.setOpen(c, true);
|
|
4087
4437
|
});
|
|
4088
4438
|
};
|
|
4089
4439
|
const keepStructureOpen = () => {
|
|
4090
4440
|
const layers = editor.Layers;
|
|
4091
4441
|
const opened = layers.model?.get("opened") ?? {};
|
|
4092
4442
|
for (const [cid, c] of Object.entries(opened)) {
|
|
4093
|
-
if (STRUCTURE.has(
|
|
4443
|
+
if (STRUCTURE.has(tagOf6(c))) delete opened[cid];
|
|
4094
4444
|
}
|
|
4095
4445
|
};
|
|
4096
4446
|
editor.on("component:add", (c) => {
|
|
@@ -4118,16 +4468,16 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4118
4468
|
editor.on("component:deselected", () => {
|
|
4119
4469
|
buildCrumbs(editor.getSelected());
|
|
4120
4470
|
});
|
|
4121
|
-
|
|
4471
|
+
const baseline = createSaveBaseline();
|
|
4122
4472
|
const save = async () => {
|
|
4123
4473
|
const view = editor.getEditing()?.getView();
|
|
4124
4474
|
await view?.syncContent?.();
|
|
4125
4475
|
const mjml = editor.getHtml();
|
|
4126
|
-
if (mjml
|
|
4476
|
+
if (!baseline.changed(mjml)) return;
|
|
4127
4477
|
setSave("saving");
|
|
4128
4478
|
try {
|
|
4129
4479
|
await documents.save(docId, { mjml, projectData: editor.getProjectData() });
|
|
4130
|
-
|
|
4480
|
+
baseline.saved(mjml);
|
|
4131
4481
|
setSave("saved");
|
|
4132
4482
|
} catch (e) {
|
|
4133
4483
|
setSave("error");
|
|
@@ -4158,6 +4508,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4158
4508
|
});
|
|
4159
4509
|
}
|
|
4160
4510
|
const lock = (on) => {
|
|
4511
|
+
if (!turn.mounted) return;
|
|
4161
4512
|
readOnlyRef.current = on;
|
|
4162
4513
|
setLocked(on);
|
|
4163
4514
|
notifyState();
|
|
@@ -4183,37 +4534,36 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4183
4534
|
return;
|
|
4184
4535
|
}
|
|
4185
4536
|
lock(false);
|
|
4537
|
+
queue.schedule();
|
|
4186
4538
|
};
|
|
4187
4539
|
const reload = () => queue.run(async () => {
|
|
4188
4540
|
const fresh = await documents.get(docId);
|
|
4189
|
-
if (fresh.mjml !== stored) loadMjml(fresh.mjml);
|
|
4541
|
+
if (fresh.mjml !== baseline.stored) loadMjml(fresh.mjml);
|
|
4190
4542
|
});
|
|
4191
4543
|
const loadMjml = (mjml) => {
|
|
4192
4544
|
loadingRef.current = true;
|
|
4193
4545
|
let repaired = false;
|
|
4546
|
+
const adds = !mjml || (0, import_core4.listSections)(mjml).some((s) => !s.sectionId);
|
|
4194
4547
|
try {
|
|
4195
4548
|
queue.cancel();
|
|
4196
|
-
|
|
4197
|
-
const source =
|
|
4198
|
-
repaired = source
|
|
4199
|
-
editor.setComponents(source);
|
|
4200
|
-
editor.getWrapper()?.find("mj-section").forEach(decorate);
|
|
4201
|
-
for (const t of Object.keys(TYPE_LABEL)) {
|
|
4202
|
-
editor.getWrapper()?.find(t).forEach(decorate);
|
|
4203
|
-
}
|
|
4549
|
+
baseline.load(mjml, null);
|
|
4550
|
+
const source = sourceForEditor(mjml || import_core4.STARTER_MJML);
|
|
4551
|
+
repaired = source.repaired;
|
|
4552
|
+
editor.setComponents(source.mjml);
|
|
4204
4553
|
editor.getWrapper()?.onAll((c) => {
|
|
4205
|
-
const illegal = illegalAttributes(
|
|
4554
|
+
const illegal = illegalAttributes(tagOf6(c), c.getAttributes());
|
|
4206
4555
|
if (illegal.length === 0) return;
|
|
4207
4556
|
c.removeAttributes(illegal);
|
|
4208
4557
|
repaired = true;
|
|
4209
4558
|
});
|
|
4559
|
+
if (!repaired && !adds) baseline.load(mjml, editor.getHtml());
|
|
4210
4560
|
showBodyInLayers();
|
|
4211
4561
|
editor.UndoManager.clear();
|
|
4212
4562
|
} finally {
|
|
4213
4563
|
loadingRef.current = false;
|
|
4214
4564
|
}
|
|
4215
4565
|
notifyState();
|
|
4216
|
-
if (repaired && !readOnlyRef.current) queue.schedule();
|
|
4566
|
+
if ((repaired || adds) && !readOnlyRef.current) queue.schedule();
|
|
4217
4567
|
};
|
|
4218
4568
|
const ensureHighlightStyles = () => {
|
|
4219
4569
|
const cdoc = editor.Canvas.getDocument();
|
|
@@ -4235,7 +4585,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4235
4585
|
cdoc.head.appendChild(style);
|
|
4236
4586
|
};
|
|
4237
4587
|
const sectionEl = (sectionId) => {
|
|
4238
|
-
|
|
4588
|
+
let comp;
|
|
4589
|
+
editor.getWrapper()?.onAll((c) => {
|
|
4590
|
+
if (!comp && isSection(c) && sectionIdOf(c) === sectionId) comp = c;
|
|
4591
|
+
});
|
|
4239
4592
|
return comp?.getEl?.() ?? void 0;
|
|
4240
4593
|
};
|
|
4241
4594
|
const compiledHtml = () => {
|
|
@@ -4246,9 +4599,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4246
4599
|
return editor.getHtml();
|
|
4247
4600
|
}
|
|
4248
4601
|
};
|
|
4602
|
+
turn.ready((on) => void applyReadOnly.current(on));
|
|
4249
4603
|
try {
|
|
4250
|
-
|
|
4251
|
-
loadMjml(doc.mjml);
|
|
4604
|
+
await queue.run(async () => loadMjml((await documents.get(docId)).mjml));
|
|
4252
4605
|
} catch (e) {
|
|
4253
4606
|
import_sonner2.toast.error(labels.documentLoadFailed);
|
|
4254
4607
|
console.error(e);
|
|
@@ -4303,7 +4656,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4303
4656
|
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4304
4657
|
import_react9.default,
|
|
4305
4658
|
{
|
|
4306
|
-
grapesjs:
|
|
4659
|
+
grapesjs: import_grapesjs2.default,
|
|
4307
4660
|
className: "h-full",
|
|
4308
4661
|
options: {
|
|
4309
4662
|
height: "100%",
|
|
@@ -4312,6 +4665,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4312
4665
|
// app.grapesjs.com and leave a session-storage entry, which a host would then have
|
|
4313
4666
|
// to disclose and, for the entry, ask consent for.
|
|
4314
4667
|
telemetry: false,
|
|
4668
|
+
canvasCss: CANVAS_CSS,
|
|
4315
4669
|
fromElement: false,
|
|
4316
4670
|
panels: { defaults: [] },
|
|
4317
4671
|
blockManager: { appendTo: "#gjs-blocks" },
|
|
@@ -4331,7 +4685,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4331
4685
|
close: () => setPicker(null)
|
|
4332
4686
|
}
|
|
4333
4687
|
},
|
|
4334
|
-
canvas: {
|
|
4688
|
+
canvas: {
|
|
4689
|
+
customBadgeLabel: badgeLabel(labels),
|
|
4690
|
+
// A file or text dragged in from outside lands as `<img>` or `<div>`, which no MJML
|
|
4691
|
+
// validator accepts; drags from the palette are unaffected.
|
|
4692
|
+
allowExternalDrop: false
|
|
4693
|
+
},
|
|
4335
4694
|
deviceManager: {
|
|
4336
4695
|
devices: [
|
|
4337
4696
|
{ id: "desktop", name: "Desktop", width: "" },
|
|
@@ -4339,14 +4698,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4339
4698
|
]
|
|
4340
4699
|
},
|
|
4341
4700
|
plugins: [
|
|
4342
|
-
(editor) => (0,
|
|
4343
|
-
blocks:
|
|
4344
|
-
block: (
|
|
4345
|
-
label: labels.blockLabels[id] ?? id,
|
|
4346
|
-
// By id: a regex on the label broke as soon as a host translated it.
|
|
4347
|
-
category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
|
|
4348
|
-
...id === "mj-social-group" && { content: SOCIAL_BLOCK }
|
|
4349
|
-
})
|
|
4701
|
+
(editor) => (0, import_grapesjs_mjml2.default)(editor, {
|
|
4702
|
+
blocks: BUILTIN_BLOCKS,
|
|
4703
|
+
block: builtinBlock(labels)
|
|
4350
4704
|
})
|
|
4351
4705
|
]
|
|
4352
4706
|
},
|
|
@@ -4428,7 +4782,7 @@ function LeftSidebar({
|
|
|
4428
4782
|
const selectBody = () => {
|
|
4429
4783
|
let body;
|
|
4430
4784
|
editor?.getWrapper()?.onAll((c) => {
|
|
4431
|
-
if (!body &&
|
|
4785
|
+
if (!body && tagOf6(c) === "mj-body") body = c;
|
|
4432
4786
|
});
|
|
4433
4787
|
if (body) editor?.select(body);
|
|
4434
4788
|
};
|
|
@@ -4436,6 +4790,13 @@ function LeftSidebar({
|
|
|
4436
4790
|
setLayersExpanded(expand);
|
|
4437
4791
|
editor?.getWrapper()?.onAll((c) => c.set("open", expand));
|
|
4438
4792
|
};
|
|
4793
|
+
const onTileKey = (e) => {
|
|
4794
|
+
const tile = e.target.closest("[data-block]");
|
|
4795
|
+
const block = tile && editor?.Blocks.get(tile.dataset.block ?? "");
|
|
4796
|
+
if (!editor || !block || !addsBlock(e)) return;
|
|
4797
|
+
e.preventDefault();
|
|
4798
|
+
if (insertBlock(editor, block)) (0, import_sonner2.toast)(labels.blockAdded(tile.textContent?.trim() ?? ""));
|
|
4799
|
+
};
|
|
4439
4800
|
const filterBlocks = (q) => {
|
|
4440
4801
|
setBlockSearch(q);
|
|
4441
4802
|
const root = document.getElementById("gjs-blocks");
|
|
@@ -4492,7 +4853,7 @@ function LeftSidebar({
|
|
|
4492
4853
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("kbd", { className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 rounded border border-panel-border bg-panel px-1 py-0.5 font-mono text-[10px] text-panel-muted-fg", children: "/" })
|
|
4493
4854
|
] }),
|
|
4494
4855
|
noBlockMatches && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "px-3 py-6 text-center text-xs text-panel-muted-fg", children: labels.noBlocksMatch }),
|
|
4495
|
-
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
|
|
4856
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: onTileKey })
|
|
4496
4857
|
] }),
|
|
4497
4858
|
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4498
4859
|
Inspector,
|
|
@@ -4573,6 +4934,7 @@ function Breadcrumb({
|
|
|
4573
4934
|
}
|
|
4574
4935
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4575
4936
|
0 && (module.exports = {
|
|
4576
|
-
EmailEditor
|
|
4937
|
+
EmailEditor,
|
|
4938
|
+
blockProblems
|
|
4577
4939
|
});
|
|
4578
4940
|
//# sourceMappingURL=canvas.cjs.map
|