@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.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
useDocumentStore,
|
|
17
17
|
useImageLibrary,
|
|
18
18
|
useLabels
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-K7KQC7LX.js";
|
|
20
20
|
import {
|
|
21
21
|
Button,
|
|
22
22
|
Input,
|
|
@@ -29,11 +29,35 @@ import {
|
|
|
29
29
|
} from "./chunk-V6RR4VRD.js";
|
|
30
30
|
|
|
31
31
|
// src/components/editor/EmailEditor.tsx
|
|
32
|
-
import
|
|
32
|
+
import grapesjs2 from "grapesjs";
|
|
33
33
|
import GjsEditor, { Canvas, useEditorMaybe as useEditorMaybe2 } from "@grapesjs/react";
|
|
34
|
-
import
|
|
34
|
+
import grapesjsMJML2 from "grapesjs-mjml";
|
|
35
35
|
import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
|
|
36
|
-
import { STARTER_MJML } from "@mjml-agent-editor/core";
|
|
36
|
+
import { listSections, STARTER_MJML } from "@mjml-agent-editor/core";
|
|
37
|
+
|
|
38
|
+
// src/lib/save-baseline.ts
|
|
39
|
+
function createSaveBaseline() {
|
|
40
|
+
let stored = "";
|
|
41
|
+
let loaded = null;
|
|
42
|
+
return {
|
|
43
|
+
/** The MJML the store holds, as far as this editor knows. */
|
|
44
|
+
get stored() {
|
|
45
|
+
return stored;
|
|
46
|
+
},
|
|
47
|
+
/** `canvas` is what the load serialises to, or null when it repaired something to write. */
|
|
48
|
+
load(mjml, canvas) {
|
|
49
|
+
stored = mjml;
|
|
50
|
+
loaded = canvas;
|
|
51
|
+
},
|
|
52
|
+
saved(mjml) {
|
|
53
|
+
stored = mjml;
|
|
54
|
+
loaded = null;
|
|
55
|
+
},
|
|
56
|
+
changed(mjml) {
|
|
57
|
+
return mjml !== stored && mjml !== loaded;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
// src/lib/save-queue.ts
|
|
39
63
|
function createSaveQueue(save, delayMs) {
|
|
@@ -64,6 +88,39 @@ function createSaveQueue(save, delayMs) {
|
|
|
64
88
|
};
|
|
65
89
|
}
|
|
66
90
|
|
|
91
|
+
// src/lib/turn-follower.ts
|
|
92
|
+
function createTurnFollower() {
|
|
93
|
+
let running = false;
|
|
94
|
+
let mounted = true;
|
|
95
|
+
let apply = null;
|
|
96
|
+
return {
|
|
97
|
+
turn(on) {
|
|
98
|
+
if (on === running) return;
|
|
99
|
+
running = on;
|
|
100
|
+
if (mounted) apply?.(on);
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* Whether the canvas is still mounted. An unmounted one's late reload must not unlock
|
|
104
|
+
* the shared state a canvas mounted since has locked.
|
|
105
|
+
*/
|
|
106
|
+
get mounted() {
|
|
107
|
+
return mounted;
|
|
108
|
+
},
|
|
109
|
+
// Both, as React mounts twice in development.
|
|
110
|
+
mount() {
|
|
111
|
+
mounted = true;
|
|
112
|
+
},
|
|
113
|
+
unmount() {
|
|
114
|
+
mounted = false;
|
|
115
|
+
},
|
|
116
|
+
/** A turn already under way is applied at once; none means nothing to apply. */
|
|
117
|
+
ready(fn) {
|
|
118
|
+
apply = fn;
|
|
119
|
+
if (running) fn(true);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
67
124
|
// src/lib/shortcuts.ts
|
|
68
125
|
function opensBlockSearch(e) {
|
|
69
126
|
if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey || e.isComposing) return false;
|
|
@@ -397,6 +454,7 @@ import {
|
|
|
397
454
|
} from "lucide-react";
|
|
398
455
|
|
|
399
456
|
// src/components/editor/inspector/values.ts
|
|
457
|
+
import { isSafeLink, isSafeNavbarLink } from "@mjml-agent-editor/core";
|
|
400
458
|
var SIDES = ["top", "right", "bottom", "left"];
|
|
401
459
|
function expandBox(value) {
|
|
402
460
|
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
@@ -427,15 +485,6 @@ function parseNetwork(name) {
|
|
|
427
485
|
function networkName(network, share) {
|
|
428
486
|
return share || network === "custom" ? network : `${network}${NO_SHARE}`;
|
|
429
487
|
}
|
|
430
|
-
var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel", "sms"]);
|
|
431
|
-
function isSafeLink(value) {
|
|
432
|
-
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, "");
|
|
433
|
-
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(decoded)?.[1];
|
|
434
|
-
return !scheme || LINK_SCHEMES.has(scheme.toLowerCase());
|
|
435
|
-
}
|
|
436
|
-
function isSafeNavbarLink(baseUrl, href) {
|
|
437
|
-
return isSafeLink(baseUrl) && isSafeLink(href) && isSafeLink(baseUrl + href);
|
|
438
|
-
}
|
|
439
488
|
|
|
440
489
|
// src/components/editor/inspector/controls.tsx
|
|
441
490
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -1241,6 +1290,23 @@ function withoutEditorProps(mjml) {
|
|
|
1241
1290
|
)
|
|
1242
1291
|
);
|
|
1243
1292
|
}
|
|
1293
|
+
function withClosedTags(mjml) {
|
|
1294
|
+
return mjml.replace(START_TAG, (tag) => {
|
|
1295
|
+
const selfClosed = /^<(mj-[\w-]+)([\s\S]*?)\s*\/>$/i.exec(tag);
|
|
1296
|
+
return selfClosed ? `<${selfClosed[1]}${selfClosed[2]}></${selfClosed[1]}>` : tag;
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
var TABLE = /<!--[\s\S]*?(?:-->|$)|(<mj-table\b(?:[^>"']|"[^"]*"|'[^']*')*>)((?:<!--[\s\S]*?-->|(?!<!--)[\s\S])*?)(<\/mj-table\s*>)/gi;
|
|
1300
|
+
function withTableRowsKept(mjml) {
|
|
1301
|
+
return mjml.replace(
|
|
1302
|
+
TABLE,
|
|
1303
|
+
(match, open, rows, close) => open ? `${open}<template>${rows}</template>${close}` : match
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
function sourceForEditor(mjml) {
|
|
1307
|
+
const cleaned = withoutEditorProps(mjml);
|
|
1308
|
+
return { mjml: withTableRowsKept(withClosedTags(cleaned)), repaired: cleaned !== mjml };
|
|
1309
|
+
}
|
|
1244
1310
|
function illegalAttributes(tag, attributes) {
|
|
1245
1311
|
const allowed = ALLOWED_ATTRIBUTES[tag];
|
|
1246
1312
|
if (!allowed) return [];
|
|
@@ -2117,7 +2183,10 @@ var ICONS = {
|
|
|
2117
2183
|
"mj-navbar-link": svg(
|
|
2118
2184
|
'<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"/>'
|
|
2119
2185
|
),
|
|
2120
|
-
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>')
|
|
2186
|
+
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>'),
|
|
2187
|
+
"mj-table": svg(
|
|
2188
|
+
'<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/>'
|
|
2189
|
+
)
|
|
2121
2190
|
};
|
|
2122
2191
|
function tagOf(c) {
|
|
2123
2192
|
return String(c.get("tagName") || c.get("type") || "");
|
|
@@ -2893,8 +2962,15 @@ function Header({
|
|
|
2893
2962
|
] });
|
|
2894
2963
|
}
|
|
2895
2964
|
|
|
2965
|
+
// src/components/editor/blocks.ts
|
|
2966
|
+
import grapesjs from "grapesjs";
|
|
2967
|
+
import grapesjsMJML from "grapesjs-mjml";
|
|
2968
|
+
|
|
2896
2969
|
// src/components/editor/ids.ts
|
|
2897
|
-
var ID_RE = {
|
|
2970
|
+
var ID_RE = {
|
|
2971
|
+
sec: /(?:^|\s)sec-([A-Za-z0-9_-]+)(?=\s|$)/,
|
|
2972
|
+
obj: /(?:^|\s)obj-([A-Za-z0-9_-]+)(?=\s|$)/
|
|
2973
|
+
};
|
|
2898
2974
|
function idOf(c, prefix) {
|
|
2899
2975
|
return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
|
|
2900
2976
|
}
|
|
@@ -2923,19 +2999,292 @@ function freshenCopiedIds(root, added) {
|
|
|
2923
2999
|
const id = idOf(c, prefix);
|
|
2924
3000
|
if (!id || !used(prefix, id, c)) continue;
|
|
2925
3001
|
const cssClass = String(c.getAttributes()["css-class"]);
|
|
2926
|
-
|
|
3002
|
+
const fresh = cssClass.replace(
|
|
3003
|
+
ID_RE[prefix],
|
|
3004
|
+
(token) => token.replace(`${prefix}-${id}`, `${prefix}-${newId()}`)
|
|
3005
|
+
);
|
|
3006
|
+
c.addAttributes({ "css-class": fresh });
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// src/components/editor/insert-block.ts
|
|
3012
|
+
var addsBlock = (e) => (e.key === "Enter" || e.key === " ") && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
3013
|
+
var tileAttributes = (id) => ({ tabindex: "0", role: "button", "data-block": id });
|
|
3014
|
+
var tagOf5 = (c) => String(c.get("tagName") ?? "");
|
|
3015
|
+
function closest(c, tag) {
|
|
3016
|
+
for (let cur = c; cur; cur = cur.parent() ?? void 0) if (tagOf5(cur) === tag) return cur;
|
|
3017
|
+
return void 0;
|
|
3018
|
+
}
|
|
3019
|
+
function childOf(parent, c) {
|
|
3020
|
+
let cur = c;
|
|
3021
|
+
while (cur.parent() && cur.parent() !== parent) cur = cur.parent();
|
|
3022
|
+
return cur;
|
|
3023
|
+
}
|
|
3024
|
+
function insertBlock(editor, block) {
|
|
3025
|
+
const content = block.getContent();
|
|
3026
|
+
let body;
|
|
3027
|
+
editor.getWrapper()?.onAll((c) => {
|
|
3028
|
+
if (!body && tagOf5(c) === "mj-body") body = c;
|
|
3029
|
+
});
|
|
3030
|
+
if (typeof content !== "string" || !body) return void 0;
|
|
3031
|
+
const selected = editor.getSelected();
|
|
3032
|
+
const section = closest(selected, "mj-section");
|
|
3033
|
+
const sections = section?.parent() ?? body;
|
|
3034
|
+
const afterSection = section ? { at: section.index() + 1 } : {};
|
|
3035
|
+
const column = closest(selected, "mj-column");
|
|
3036
|
+
let added;
|
|
3037
|
+
if (/^\s*<mj-section\b/i.test(content)) {
|
|
3038
|
+
added = sections.append(content, afterSection)[0];
|
|
3039
|
+
} else if (column && selected) {
|
|
3040
|
+
const at = selected === column ? {} : { at: childOf(column, selected).index() + 1 };
|
|
3041
|
+
added = column.append(content, at)[0];
|
|
3042
|
+
} else {
|
|
3043
|
+
const [wrapper] = sections.append(
|
|
3044
|
+
`<mj-section><mj-column>${content}</mj-column></mj-section>`,
|
|
3045
|
+
afterSection
|
|
3046
|
+
);
|
|
3047
|
+
added = wrapper?.components().at(0)?.components().at(0);
|
|
3048
|
+
}
|
|
3049
|
+
if (!added) return void 0;
|
|
3050
|
+
editor.select(added);
|
|
3051
|
+
if (block.get("activate")) added.getView()?.onActive(void 0);
|
|
3052
|
+
return added;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// src/components/editor/blocks.ts
|
|
3056
|
+
var EDITABLE_TAGS = /* @__PURE__ */ new Set([
|
|
3057
|
+
"mj-section",
|
|
3058
|
+
"mj-group",
|
|
3059
|
+
"mj-column",
|
|
3060
|
+
"mj-text",
|
|
3061
|
+
"mj-image",
|
|
3062
|
+
"mj-button",
|
|
3063
|
+
"mj-divider",
|
|
3064
|
+
"mj-spacer",
|
|
3065
|
+
"mj-social",
|
|
3066
|
+
"mj-social-element",
|
|
3067
|
+
"mj-navbar",
|
|
3068
|
+
"mj-navbar-link"
|
|
3069
|
+
]);
|
|
3070
|
+
var CONTENT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-social-element", "mj-navbar-link"]);
|
|
3071
|
+
var EDITOR_INTERNALS = /data-gjs-|__p\b|custom-name/i;
|
|
3072
|
+
var DROPPED_MARKUP = /<(?:script|style|link|meta|iframe|object|embed|form)\b/i;
|
|
3073
|
+
var INLINE_IMAGE = /\b(?:src|srcset|background-url|icon)\s*=\s*(?:"[^"]*data:|'[^']*data:|data:)/i;
|
|
3074
|
+
var LINK_IN_MARKUP = /[\s/"'](?:xlink:)?(?:href|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
3075
|
+
var validator;
|
|
3076
|
+
function blockProblems(mjml, editor = validator ??= grapesjs.init({
|
|
3077
|
+
headless: true,
|
|
3078
|
+
storageManager: false,
|
|
3079
|
+
telemetry: false,
|
|
3080
|
+
plugins: [grapesjsMJML]
|
|
3081
|
+
})) {
|
|
3082
|
+
if (EDITOR_INTERNALS.test(mjml)) return ["it contains GrapesJS's own attributes"];
|
|
3083
|
+
if (INLINE_IMAGE.test(mjml)) return ["it inlines an image as a data: URL"];
|
|
3084
|
+
let tree;
|
|
3085
|
+
try {
|
|
3086
|
+
tree = editor.runCommand("mjml-code-to-html", {
|
|
3087
|
+
mjml: `<mjml><mj-body>${withClosedTags(mjml)}</mj-body></mjml>`,
|
|
3088
|
+
validationLevel: "strict"
|
|
3089
|
+
}).json;
|
|
3090
|
+
} catch (e) {
|
|
3091
|
+
const errors = e.errors;
|
|
3092
|
+
if (!errors) throw e;
|
|
3093
|
+
return errors.map((error) => error.formattedMessage ?? error.message ?? String(error));
|
|
3094
|
+
}
|
|
3095
|
+
const problems = [];
|
|
3096
|
+
const top = tree.children?.find((c) => c.tagName === "mj-body")?.children ?? [];
|
|
3097
|
+
if (top.length !== 1 || top[0].tagName !== "mj-section") {
|
|
3098
|
+
problems.push("it is not exactly one mj-section");
|
|
3099
|
+
}
|
|
3100
|
+
const visit = (node, parent) => {
|
|
3101
|
+
const tag = node.tagName;
|
|
3102
|
+
const attributes = node.attributes ?? {};
|
|
3103
|
+
const content = node.content ?? "";
|
|
3104
|
+
if (!EDITABLE_TAGS.has(tag)) problems.push(`${tag} is not editable here`);
|
|
3105
|
+
const cssClass = attributes["css-class"] ?? "";
|
|
3106
|
+
if (ID_RE.sec.test(cssClass) || ID_RE.obj.test(cssClass)) {
|
|
3107
|
+
problems.push(`${tag} carries a sec-* or obj-* id`);
|
|
3108
|
+
}
|
|
3109
|
+
if ("mj-class" in attributes) problems.push(`${tag} uses mj-class`);
|
|
3110
|
+
const links = [attributes.href ?? ""];
|
|
3111
|
+
for (const match of content.matchAll(LINK_IN_MARKUP)) {
|
|
3112
|
+
links.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
3113
|
+
}
|
|
3114
|
+
const linked = tag === "mj-navbar-link" ? isSafeNavbarLink(parent?.attributes?.["base-url"] ?? "", attributes.href ?? "") : true;
|
|
3115
|
+
if (!linked || !links.every(isSafeLink)) {
|
|
3116
|
+
problems.push(`${tag} has a link scheme mail must not carry`);
|
|
3117
|
+
}
|
|
3118
|
+
if (CONTENT_TAGS.has(tag) ? /<mj-/i.test(content) || DROPPED_MARKUP.test(content) : content.trim()) {
|
|
3119
|
+
problems.push(`${tag} holds content the editor does not keep`);
|
|
3120
|
+
}
|
|
3121
|
+
node.children?.forEach((child) => visit(child, node));
|
|
3122
|
+
};
|
|
3123
|
+
top.forEach((node) => visit(node));
|
|
3124
|
+
return problems;
|
|
3125
|
+
}
|
|
3126
|
+
var escapeHtml3 = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3127
|
+
function registerBlocks(editor, blocks, category, media) {
|
|
3128
|
+
for (const block of blocks) {
|
|
3129
|
+
const problems = editor.Blocks.get(block.id) ? [`the id is already taken`] : blockProblems(block.mjml, editor);
|
|
3130
|
+
if (problems.length > 0) {
|
|
3131
|
+
console.error(`Block "${block.id}" is left out of the palette: ${problems.join("; ")}`);
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
editor.Blocks.add(block.id, {
|
|
3135
|
+
// GrapesJS writes the label into the tile as markup.
|
|
3136
|
+
label: escapeHtml3(block.label),
|
|
3137
|
+
category,
|
|
3138
|
+
media: block.media ?? media,
|
|
3139
|
+
content: withClosedTags(block.mjml),
|
|
3140
|
+
attributes: tileAttributes(block.id)
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
// src/components/editor/builtin-blocks.ts
|
|
3146
|
+
var BUILTIN_BLOCKS = [
|
|
3147
|
+
"mj-1-column",
|
|
3148
|
+
"mj-2-columns",
|
|
3149
|
+
"mj-3-columns",
|
|
3150
|
+
"mj-text",
|
|
3151
|
+
"mj-image",
|
|
3152
|
+
"mj-button",
|
|
3153
|
+
"mj-divider",
|
|
3154
|
+
"mj-spacer",
|
|
3155
|
+
"mj-social-group",
|
|
3156
|
+
"mj-navbar"
|
|
3157
|
+
];
|
|
3158
|
+
var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
|
|
3159
|
+
var CONTENT2 = {
|
|
3160
|
+
// The plugin's version links Google+ and shares the page: a bare network name makes the
|
|
3161
|
+
// icon open a "share this link" dialog, while `-noshare` links to the profile a footer
|
|
3162
|
+
// means. Without `alt` each link is an unnamed image to a screen reader.
|
|
3163
|
+
"mj-social-group": `<mj-social icon-size="24px" mode="horizontal">
|
|
3164
|
+
<mj-social-element name="facebook-noshare" href="https://facebook.com/" alt="Facebook"></mj-social-element>
|
|
3165
|
+
<mj-social-element name="instagram-noshare" href="https://instagram.com/" alt="Instagram"></mj-social-element>
|
|
3166
|
+
<mj-social-element name="x-noshare" href="https://x.com/" alt="X"></mj-social-element>
|
|
3167
|
+
</mj-social>`,
|
|
3168
|
+
// The plugin's sample is a via.placeholder.com picture, stored in the email and loaded by
|
|
3169
|
+
// every reader's mail client. Dropping one opens the image picker instead.
|
|
3170
|
+
"mj-image": "<mj-image></mj-image>"
|
|
3171
|
+
};
|
|
3172
|
+
function builtinBlock(labels) {
|
|
3173
|
+
return (id) => ({
|
|
3174
|
+
label: labels.blockLabels[id] ?? id,
|
|
3175
|
+
// By id: a regex on the label broke as soon as a host translated it.
|
|
3176
|
+
category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
|
|
3177
|
+
...id in CONTENT2 && { content: CONTENT2[id] },
|
|
3178
|
+
attributes: tileAttributes(id)
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
var CANVAS_CSS = 'img:not([src]), img[src=""] { min-height: 120px; background: #f4f4f5; outline: 1px dashed #a1a1aa; outline-offset: -1px; }';
|
|
3182
|
+
|
|
3183
|
+
// src/components/editor/table-type.ts
|
|
3184
|
+
import { isSafeLink as isSafeLink2 } from "@mjml-agent-editor/core";
|
|
3185
|
+
var DROPPED = "script, style, link, iframe, frame, object, embed, applet, base, meta, animate, set, svg, math, template";
|
|
3186
|
+
var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href", "action", "formaction"]);
|
|
3187
|
+
function cleanRows(rows) {
|
|
3188
|
+
rows.querySelectorAll(DROPPED).forEach((el) => el.remove());
|
|
3189
|
+
rows.querySelectorAll("*").forEach((el) => {
|
|
3190
|
+
for (const { name, value } of [...el.attributes]) {
|
|
3191
|
+
const lower = name.toLowerCase();
|
|
3192
|
+
const script = /^(java|vb)script:/i.test(value.replace(/[\u0000-\u0020]/g, ""));
|
|
3193
|
+
if (lower.startsWith("on") || script || LINK_ATTRIBUTES.has(lower) && !isSafeLink2(value)) {
|
|
3194
|
+
el.removeAttribute(name);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
var ROW_STYLE = "pointer-events: all; display: table; width: 100%;";
|
|
3200
|
+
function tablePreview(doc, attributes, rows) {
|
|
3201
|
+
const attr = (name, fallback) => String(attributes[name] ?? fallback);
|
|
3202
|
+
const template = doc.createElement("template");
|
|
3203
|
+
template.innerHTML = `<table>${rows}</table>`;
|
|
3204
|
+
const table = doc.createElement("table");
|
|
3205
|
+
table.setAttribute("cellpadding", String(parseInt(attr("cellpadding", "0"), 10) || 0));
|
|
3206
|
+
table.setAttribute("cellspacing", String(parseInt(attr("cellspacing", "0"), 10) || 0));
|
|
3207
|
+
Object.assign(table.style, {
|
|
3208
|
+
width: attr("width", "100%"),
|
|
3209
|
+
border: attr("border", "none"),
|
|
3210
|
+
color: attr("color", "#000000"),
|
|
3211
|
+
fontFamily: attr("font-family", "Ubuntu, Helvetica, Arial, sans-serif"),
|
|
3212
|
+
fontSize: attr("font-size", "13px"),
|
|
3213
|
+
lineHeight: attr("line-height", "22px"),
|
|
3214
|
+
tableLayout: attr("table-layout", "auto")
|
|
3215
|
+
});
|
|
3216
|
+
for (const row of template.content.querySelectorAll("tr")) {
|
|
3217
|
+
const tr = table.insertRow();
|
|
3218
|
+
for (const cell2 of row.querySelectorAll(":scope > th, :scope > td")) {
|
|
3219
|
+
const copy = doc.createElement(cell2.tagName.toLowerCase());
|
|
3220
|
+
copy.textContent = cell2.textContent;
|
|
3221
|
+
const align = cell2.getAttribute("align");
|
|
3222
|
+
if (align && /^(left|right|center)$/i.test(align)) copy.setAttribute("align", align);
|
|
3223
|
+
tr.append(copy);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
const cell = doc.createElement("td");
|
|
3227
|
+
cell.style.padding = attr("padding", "10px 25px");
|
|
3228
|
+
cell.append(table);
|
|
3229
|
+
return cell;
|
|
3230
|
+
}
|
|
3231
|
+
function addTableType(editor) {
|
|
3232
|
+
const base = editor.Components.getType("default").view.prototype;
|
|
3233
|
+
editor.Components.addType("mj-table", {
|
|
3234
|
+
isComponent: (el) => {
|
|
3235
|
+
if (el.tagName?.toLowerCase() !== "mj-table") return false;
|
|
3236
|
+
const rows = el.querySelector(":scope > template");
|
|
3237
|
+
if (rows) cleanRows(rows.content);
|
|
3238
|
+
return { type: "mj-table", content: rows?.innerHTML ?? "", components: [] };
|
|
3239
|
+
},
|
|
3240
|
+
model: { defaults: { tagName: "mj-table", droppable: false, editable: false } },
|
|
3241
|
+
view: {
|
|
3242
|
+
tagName: "tr",
|
|
3243
|
+
// GrapesJS clears the element's attributes, style among them, when the model's change.
|
|
3244
|
+
updateAttributes(...args) {
|
|
3245
|
+
base.updateAttributes.apply(this, args);
|
|
3246
|
+
this.el.style.cssText = ROW_STYLE;
|
|
3247
|
+
},
|
|
3248
|
+
updateContent() {
|
|
3249
|
+
const { el, model } = this;
|
|
3250
|
+
el.style.cssText = ROW_STYLE;
|
|
3251
|
+
el.replaceChildren(
|
|
3252
|
+
tablePreview(el.ownerDocument, model.getAttributes(), String(model.get("content") ?? ""))
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
2927
3255
|
}
|
|
2928
3256
|
});
|
|
2929
3257
|
}
|
|
2930
3258
|
|
|
2931
3259
|
// src/components/editor/plugin-types.ts
|
|
3260
|
+
var COLUMN_CHILDREN = /* @__PURE__ */ new Set([
|
|
3261
|
+
"mj-accordion",
|
|
3262
|
+
"mj-button",
|
|
3263
|
+
"mj-carousel",
|
|
3264
|
+
"mj-divider",
|
|
3265
|
+
"mj-image",
|
|
3266
|
+
"mj-navbar",
|
|
3267
|
+
"mj-raw",
|
|
3268
|
+
"mj-social",
|
|
3269
|
+
"mj-spacer",
|
|
3270
|
+
"mj-table",
|
|
3271
|
+
"mj-text"
|
|
3272
|
+
]);
|
|
2932
3273
|
function fixPluginTypes(editor) {
|
|
3274
|
+
addTableType(editor);
|
|
2933
3275
|
for (const type of editor.Components.getTypes()) {
|
|
2934
|
-
if (!type.id.startsWith("mj-")) continue;
|
|
3276
|
+
if (!type.id.startsWith("mj-") || type.id === "mj-table") continue;
|
|
2935
3277
|
const base = type.model.prototype.getAttrToHTML;
|
|
2936
3278
|
editor.Components.addType(type.id, {
|
|
2937
3279
|
model: {
|
|
2938
|
-
defaults: {
|
|
3280
|
+
defaults: {
|
|
3281
|
+
"style-default": {},
|
|
3282
|
+
...type.id === "mj-column" && {
|
|
3283
|
+
droppable: (source) => COLUMN_CHILDREN.has(String(source.get("tagName")))
|
|
3284
|
+
},
|
|
3285
|
+
// GrapesJS's own is an SVG's markup, which the canvas wrote into `src` and saved.
|
|
3286
|
+
...type.id === "mj-image" && { src: "" }
|
|
3287
|
+
},
|
|
2939
3288
|
getAttrToHTML() {
|
|
2940
3289
|
const attributes = base.call(this);
|
|
2941
3290
|
delete attributes.__p;
|
|
@@ -2957,16 +3306,17 @@ var TYPE_LABEL = {
|
|
|
2957
3306
|
"mj-image": "Image",
|
|
2958
3307
|
"mj-column": "Column"
|
|
2959
3308
|
};
|
|
2960
|
-
function
|
|
3309
|
+
function tagOf6(c) {
|
|
2961
3310
|
return String(c.get("tagName") || c.get("type") || "");
|
|
2962
3311
|
}
|
|
2963
3312
|
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>';
|
|
2964
3313
|
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>';
|
|
3314
|
+
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>';
|
|
2965
3315
|
function isSection(c) {
|
|
2966
|
-
return
|
|
3316
|
+
return tagOf6(c) === "mj-section";
|
|
2967
3317
|
}
|
|
2968
3318
|
function isCommentable(c) {
|
|
2969
|
-
const t =
|
|
3319
|
+
const t = tagOf6(c);
|
|
2970
3320
|
return t === "mj-section" || t in TYPE_LABEL;
|
|
2971
3321
|
}
|
|
2972
3322
|
function closestSection(c) {
|
|
@@ -2978,29 +3328,11 @@ function closestSection(c) {
|
|
|
2978
3328
|
return null;
|
|
2979
3329
|
}
|
|
2980
3330
|
function crumbName(c, labels) {
|
|
2981
|
-
const t =
|
|
3331
|
+
const t = tagOf6(c);
|
|
2982
3332
|
return labels.elementNames[t] ?? t.replace(/^mj-/, "");
|
|
2983
3333
|
}
|
|
2984
|
-
var BLOCKS = [
|
|
2985
|
-
"mj-1-column",
|
|
2986
|
-
"mj-2-columns",
|
|
2987
|
-
"mj-3-columns",
|
|
2988
|
-
"mj-text",
|
|
2989
|
-
"mj-image",
|
|
2990
|
-
"mj-button",
|
|
2991
|
-
"mj-divider",
|
|
2992
|
-
"mj-spacer",
|
|
2993
|
-
"mj-social-group",
|
|
2994
|
-
"mj-navbar"
|
|
2995
|
-
];
|
|
2996
|
-
var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
|
|
2997
|
-
var SOCIAL_BLOCK = `<mj-social icon-size="24px" mode="horizontal">
|
|
2998
|
-
<mj-social-element name="facebook-noshare" href="https://facebook.com/"></mj-social-element>
|
|
2999
|
-
<mj-social-element name="instagram-noshare" href="https://instagram.com/"></mj-social-element>
|
|
3000
|
-
<mj-social-element name="x-noshare" href="https://x.com/"></mj-social-element>
|
|
3001
|
-
</mj-social>`;
|
|
3002
3334
|
function objectLabel(c) {
|
|
3003
|
-
const base = TYPE_LABEL[
|
|
3335
|
+
const base = TYPE_LABEL[tagOf6(c)] ?? tagOf6(c);
|
|
3004
3336
|
const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
|
|
3005
3337
|
return text ? `${base}: \u201C${text}\u201D` : base;
|
|
3006
3338
|
}
|
|
@@ -3068,7 +3400,13 @@ function setupRichText(editor, labels) {
|
|
|
3068
3400
|
result: (r, action) => r.exec("hiliteColor", val(action, "input"))
|
|
3069
3401
|
});
|
|
3070
3402
|
}
|
|
3071
|
-
function EmailEditor({
|
|
3403
|
+
function EmailEditor({
|
|
3404
|
+
docId,
|
|
3405
|
+
onReady,
|
|
3406
|
+
commentsRefresh,
|
|
3407
|
+
onOpenCountChange,
|
|
3408
|
+
blocks = []
|
|
3409
|
+
}) {
|
|
3072
3410
|
const documents = useDocumentStore();
|
|
3073
3411
|
const labels = useLabels();
|
|
3074
3412
|
const loadingRef = useRef5(false);
|
|
@@ -3090,8 +3428,15 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3090
3428
|
const readOnlyRef = useRef5(false);
|
|
3091
3429
|
const applyReadOnly = useRef5(async () => {
|
|
3092
3430
|
});
|
|
3093
|
-
|
|
3094
|
-
useEffect5(() => ()
|
|
3431
|
+
const [turn] = useState5(createTurnFollower);
|
|
3432
|
+
useEffect5(() => turn.turn(turnRunning), [turn, turnRunning]);
|
|
3433
|
+
useEffect5(() => {
|
|
3434
|
+
turn.mount();
|
|
3435
|
+
return () => {
|
|
3436
|
+
turn.unmount();
|
|
3437
|
+
setLocked(false);
|
|
3438
|
+
};
|
|
3439
|
+
}, [turn, setLocked]);
|
|
3095
3440
|
const [crumbs, setCrumbs] = useState5([]);
|
|
3096
3441
|
const [picker, setPicker] = useState5(
|
|
3097
3442
|
null
|
|
@@ -3102,7 +3447,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3102
3447
|
const chain = [];
|
|
3103
3448
|
let cur = c;
|
|
3104
3449
|
while (cur) {
|
|
3105
|
-
const t =
|
|
3450
|
+
const t = tagOf6(cur);
|
|
3106
3451
|
if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
|
|
3107
3452
|
chain.unshift(cur);
|
|
3108
3453
|
cur = cur.parent() ?? void 0;
|
|
@@ -3151,7 +3496,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3151
3496
|
if (isSection(c)) addIdClass(c, "sec");
|
|
3152
3497
|
if (!isCommentable(c)) return;
|
|
3153
3498
|
const toolbar = [...c.get("toolbar") ?? []];
|
|
3154
|
-
if (
|
|
3499
|
+
if (tagOf6(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
|
|
3155
3500
|
toolbar.push({
|
|
3156
3501
|
command: "open-image-picker",
|
|
3157
3502
|
label: IMAGE_SVG,
|
|
@@ -3171,7 +3516,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3171
3516
|
editor.Commands.add("open-image-picker", {
|
|
3172
3517
|
run(ed) {
|
|
3173
3518
|
const target = ed.getSelected();
|
|
3174
|
-
if (!target ||
|
|
3519
|
+
if (!target || tagOf6(target) !== "mj-image") return;
|
|
3175
3520
|
const am = ed.AssetManager;
|
|
3176
3521
|
am.open({
|
|
3177
3522
|
target,
|
|
@@ -3191,6 +3536,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3191
3536
|
});
|
|
3192
3537
|
setupRichText(editor, labels);
|
|
3193
3538
|
fixPluginTypes(editor);
|
|
3539
|
+
registerBlocks(editor, blocks, labels.blockCategorySections, SECTION_SVG);
|
|
3194
3540
|
setupLayerNames(editor, labels);
|
|
3195
3541
|
const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
|
|
3196
3542
|
const showBodyInLayers = () => {
|
|
@@ -3198,14 +3544,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3198
3544
|
if (!body) return;
|
|
3199
3545
|
editor.Layers.setRoot(body);
|
|
3200
3546
|
body.onAll((c) => {
|
|
3201
|
-
if (STRUCTURE.has(
|
|
3547
|
+
if (STRUCTURE.has(tagOf6(c))) editor.Layers.setOpen(c, true);
|
|
3202
3548
|
});
|
|
3203
3549
|
};
|
|
3204
3550
|
const keepStructureOpen = () => {
|
|
3205
3551
|
const layers = editor.Layers;
|
|
3206
3552
|
const opened = layers.model?.get("opened") ?? {};
|
|
3207
3553
|
for (const [cid, c] of Object.entries(opened)) {
|
|
3208
|
-
if (STRUCTURE.has(
|
|
3554
|
+
if (STRUCTURE.has(tagOf6(c))) delete opened[cid];
|
|
3209
3555
|
}
|
|
3210
3556
|
};
|
|
3211
3557
|
editor.on("component:add", (c) => {
|
|
@@ -3233,16 +3579,16 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3233
3579
|
editor.on("component:deselected", () => {
|
|
3234
3580
|
buildCrumbs(editor.getSelected());
|
|
3235
3581
|
});
|
|
3236
|
-
|
|
3582
|
+
const baseline = createSaveBaseline();
|
|
3237
3583
|
const save = async () => {
|
|
3238
3584
|
const view = editor.getEditing()?.getView();
|
|
3239
3585
|
await view?.syncContent?.();
|
|
3240
3586
|
const mjml = editor.getHtml();
|
|
3241
|
-
if (mjml
|
|
3587
|
+
if (!baseline.changed(mjml)) return;
|
|
3242
3588
|
setSave("saving");
|
|
3243
3589
|
try {
|
|
3244
3590
|
await documents.save(docId, { mjml, projectData: editor.getProjectData() });
|
|
3245
|
-
|
|
3591
|
+
baseline.saved(mjml);
|
|
3246
3592
|
setSave("saved");
|
|
3247
3593
|
} catch (e) {
|
|
3248
3594
|
setSave("error");
|
|
@@ -3273,6 +3619,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3273
3619
|
});
|
|
3274
3620
|
}
|
|
3275
3621
|
const lock = (on) => {
|
|
3622
|
+
if (!turn.mounted) return;
|
|
3276
3623
|
readOnlyRef.current = on;
|
|
3277
3624
|
setLocked(on);
|
|
3278
3625
|
notifyState();
|
|
@@ -3298,37 +3645,36 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3298
3645
|
return;
|
|
3299
3646
|
}
|
|
3300
3647
|
lock(false);
|
|
3648
|
+
queue.schedule();
|
|
3301
3649
|
};
|
|
3302
3650
|
const reload = () => queue.run(async () => {
|
|
3303
3651
|
const fresh = await documents.get(docId);
|
|
3304
|
-
if (fresh.mjml !== stored) loadMjml(fresh.mjml);
|
|
3652
|
+
if (fresh.mjml !== baseline.stored) loadMjml(fresh.mjml);
|
|
3305
3653
|
});
|
|
3306
3654
|
const loadMjml = (mjml) => {
|
|
3307
3655
|
loadingRef.current = true;
|
|
3308
3656
|
let repaired = false;
|
|
3657
|
+
const adds = !mjml || listSections(mjml).some((s) => !s.sectionId);
|
|
3309
3658
|
try {
|
|
3310
3659
|
queue.cancel();
|
|
3311
|
-
|
|
3312
|
-
const source =
|
|
3313
|
-
repaired = source
|
|
3314
|
-
editor.setComponents(source);
|
|
3315
|
-
editor.getWrapper()?.find("mj-section").forEach(decorate);
|
|
3316
|
-
for (const t of Object.keys(TYPE_LABEL)) {
|
|
3317
|
-
editor.getWrapper()?.find(t).forEach(decorate);
|
|
3318
|
-
}
|
|
3660
|
+
baseline.load(mjml, null);
|
|
3661
|
+
const source = sourceForEditor(mjml || STARTER_MJML);
|
|
3662
|
+
repaired = source.repaired;
|
|
3663
|
+
editor.setComponents(source.mjml);
|
|
3319
3664
|
editor.getWrapper()?.onAll((c) => {
|
|
3320
|
-
const illegal = illegalAttributes(
|
|
3665
|
+
const illegal = illegalAttributes(tagOf6(c), c.getAttributes());
|
|
3321
3666
|
if (illegal.length === 0) return;
|
|
3322
3667
|
c.removeAttributes(illegal);
|
|
3323
3668
|
repaired = true;
|
|
3324
3669
|
});
|
|
3670
|
+
if (!repaired && !adds) baseline.load(mjml, editor.getHtml());
|
|
3325
3671
|
showBodyInLayers();
|
|
3326
3672
|
editor.UndoManager.clear();
|
|
3327
3673
|
} finally {
|
|
3328
3674
|
loadingRef.current = false;
|
|
3329
3675
|
}
|
|
3330
3676
|
notifyState();
|
|
3331
|
-
if (repaired && !readOnlyRef.current) queue.schedule();
|
|
3677
|
+
if ((repaired || adds) && !readOnlyRef.current) queue.schedule();
|
|
3332
3678
|
};
|
|
3333
3679
|
const ensureHighlightStyles = () => {
|
|
3334
3680
|
const cdoc = editor.Canvas.getDocument();
|
|
@@ -3350,7 +3696,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3350
3696
|
cdoc.head.appendChild(style);
|
|
3351
3697
|
};
|
|
3352
3698
|
const sectionEl = (sectionId) => {
|
|
3353
|
-
|
|
3699
|
+
let comp;
|
|
3700
|
+
editor.getWrapper()?.onAll((c) => {
|
|
3701
|
+
if (!comp && isSection(c) && sectionIdOf(c) === sectionId) comp = c;
|
|
3702
|
+
});
|
|
3354
3703
|
return comp?.getEl?.() ?? void 0;
|
|
3355
3704
|
};
|
|
3356
3705
|
const compiledHtml = () => {
|
|
@@ -3361,9 +3710,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3361
3710
|
return editor.getHtml();
|
|
3362
3711
|
}
|
|
3363
3712
|
};
|
|
3713
|
+
turn.ready((on) => void applyReadOnly.current(on));
|
|
3364
3714
|
try {
|
|
3365
|
-
|
|
3366
|
-
loadMjml(doc.mjml);
|
|
3715
|
+
await queue.run(async () => loadMjml((await documents.get(docId)).mjml));
|
|
3367
3716
|
} catch (e) {
|
|
3368
3717
|
toast2.error(labels.documentLoadFailed);
|
|
3369
3718
|
console.error(e);
|
|
@@ -3418,7 +3767,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3418
3767
|
return /* @__PURE__ */ jsx8(
|
|
3419
3768
|
GjsEditor,
|
|
3420
3769
|
{
|
|
3421
|
-
grapesjs,
|
|
3770
|
+
grapesjs: grapesjs2,
|
|
3422
3771
|
className: "h-full",
|
|
3423
3772
|
options: {
|
|
3424
3773
|
height: "100%",
|
|
@@ -3427,6 +3776,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3427
3776
|
// app.grapesjs.com and leave a session-storage entry, which a host would then have
|
|
3428
3777
|
// to disclose and, for the entry, ask consent for.
|
|
3429
3778
|
telemetry: false,
|
|
3779
|
+
canvasCss: CANVAS_CSS,
|
|
3430
3780
|
fromElement: false,
|
|
3431
3781
|
panels: { defaults: [] },
|
|
3432
3782
|
blockManager: { appendTo: "#gjs-blocks" },
|
|
@@ -3446,7 +3796,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3446
3796
|
close: () => setPicker(null)
|
|
3447
3797
|
}
|
|
3448
3798
|
},
|
|
3449
|
-
canvas: {
|
|
3799
|
+
canvas: {
|
|
3800
|
+
customBadgeLabel: badgeLabel(labels),
|
|
3801
|
+
// A file or text dragged in from outside lands as `<img>` or `<div>`, which no MJML
|
|
3802
|
+
// validator accepts; drags from the palette are unaffected.
|
|
3803
|
+
allowExternalDrop: false
|
|
3804
|
+
},
|
|
3450
3805
|
deviceManager: {
|
|
3451
3806
|
devices: [
|
|
3452
3807
|
{ id: "desktop", name: "Desktop", width: "" },
|
|
@@ -3454,14 +3809,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3454
3809
|
]
|
|
3455
3810
|
},
|
|
3456
3811
|
plugins: [
|
|
3457
|
-
(editor) =>
|
|
3458
|
-
blocks:
|
|
3459
|
-
block: (
|
|
3460
|
-
label: labels.blockLabels[id] ?? id,
|
|
3461
|
-
// By id: a regex on the label broke as soon as a host translated it.
|
|
3462
|
-
category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
|
|
3463
|
-
...id === "mj-social-group" && { content: SOCIAL_BLOCK }
|
|
3464
|
-
})
|
|
3812
|
+
(editor) => grapesjsMJML2(editor, {
|
|
3813
|
+
blocks: BUILTIN_BLOCKS,
|
|
3814
|
+
block: builtinBlock(labels)
|
|
3465
3815
|
})
|
|
3466
3816
|
]
|
|
3467
3817
|
},
|
|
@@ -3543,7 +3893,7 @@ function LeftSidebar({
|
|
|
3543
3893
|
const selectBody = () => {
|
|
3544
3894
|
let body;
|
|
3545
3895
|
editor?.getWrapper()?.onAll((c) => {
|
|
3546
|
-
if (!body &&
|
|
3896
|
+
if (!body && tagOf6(c) === "mj-body") body = c;
|
|
3547
3897
|
});
|
|
3548
3898
|
if (body) editor?.select(body);
|
|
3549
3899
|
};
|
|
@@ -3551,6 +3901,13 @@ function LeftSidebar({
|
|
|
3551
3901
|
setLayersExpanded(expand);
|
|
3552
3902
|
editor?.getWrapper()?.onAll((c) => c.set("open", expand));
|
|
3553
3903
|
};
|
|
3904
|
+
const onTileKey = (e) => {
|
|
3905
|
+
const tile = e.target.closest("[data-block]");
|
|
3906
|
+
const block = tile && editor?.Blocks.get(tile.dataset.block ?? "");
|
|
3907
|
+
if (!editor || !block || !addsBlock(e)) return;
|
|
3908
|
+
e.preventDefault();
|
|
3909
|
+
if (insertBlock(editor, block)) toast2(labels.blockAdded(tile.textContent?.trim() ?? ""));
|
|
3910
|
+
};
|
|
3554
3911
|
const filterBlocks = (q) => {
|
|
3555
3912
|
setBlockSearch(q);
|
|
3556
3913
|
const root = document.getElementById("gjs-blocks");
|
|
@@ -3607,7 +3964,7 @@ function LeftSidebar({
|
|
|
3607
3964
|
/* @__PURE__ */ jsx8("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: "/" })
|
|
3608
3965
|
] }),
|
|
3609
3966
|
noBlockMatches && /* @__PURE__ */ jsx8("p", { className: "px-3 py-6 text-center text-xs text-panel-muted-fg", children: labels.noBlocksMatch }),
|
|
3610
|
-
/* @__PURE__ */ jsx8("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
|
|
3967
|
+
/* @__PURE__ */ jsx8("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: onTileKey })
|
|
3611
3968
|
] }),
|
|
3612
3969
|
/* @__PURE__ */ jsx8("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: /* @__PURE__ */ jsx8(
|
|
3613
3970
|
Inspector,
|
|
@@ -3687,6 +4044,7 @@ function Breadcrumb({
|
|
|
3687
4044
|
] }, i)) });
|
|
3688
4045
|
}
|
|
3689
4046
|
export {
|
|
3690
|
-
EmailEditor
|
|
4047
|
+
EmailEditor,
|
|
4048
|
+
blockProblems
|
|
3691
4049
|
};
|
|
3692
4050
|
//# sourceMappingURL=canvas.js.map
|