@mjml-agent-editor/editor 0.2.0 → 0.4.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 +34 -2
- package/dist/canvas.cjs +597 -226
- 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 +589 -222
- package/dist/canvas.js.map +1 -1
- package/dist/{chunk-FTEJNUJL.js → chunk-PXFZVLNQ.js} +5 -2
- package/dist/chunk-PXFZVLNQ.js.map +1 -0
- package/dist/index.cjs +4 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- 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;
|
|
@@ -1063,7 +1124,8 @@ function ImagePicker({
|
|
|
1063
1124
|
const accepts = (file) => matchesAccept(file, library?.accept);
|
|
1064
1125
|
const full = quota !== void 0 && quota.used >= quota.limit;
|
|
1065
1126
|
const upload = async (files) => {
|
|
1066
|
-
|
|
1127
|
+
const send = library?.upload?.bind(library);
|
|
1128
|
+
if (!send || !files?.length) return;
|
|
1067
1129
|
const mine = session.current;
|
|
1068
1130
|
const list = Array.from(files).filter((f) => {
|
|
1069
1131
|
if (accepts(f)) return true;
|
|
@@ -1072,7 +1134,7 @@ function ImagePicker({
|
|
|
1072
1134
|
});
|
|
1073
1135
|
if (!list.length) return;
|
|
1074
1136
|
setPending((n) => n + list.length);
|
|
1075
|
-
const results = await Promise.allSettled(list.map(async (f) =>
|
|
1137
|
+
const results = await Promise.allSettled(list.map(async (f) => send(f)));
|
|
1076
1138
|
setPending((n) => n - list.length);
|
|
1077
1139
|
for (const r of results) {
|
|
1078
1140
|
if (r.status === "rejected") {
|
|
@@ -1107,155 +1169,163 @@ function ImagePicker({
|
|
|
1107
1169
|
setDragging(false);
|
|
1108
1170
|
if (!full) void upload(e.dataTransfer.files);
|
|
1109
1171
|
};
|
|
1110
|
-
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
"
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
},
|
|
1123
|
-
|
|
1124
|
-
onDrop,
|
|
1125
|
-
className: cn(
|
|
1126
|
-
"flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
|
|
1127
|
-
dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
|
|
1128
|
-
),
|
|
1129
|
-
children: [
|
|
1130
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
|
|
1131
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
|
|
1132
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: labels.imagePickerDrop }),
|
|
1133
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1134
|
-
Button,
|
|
1135
|
-
{
|
|
1136
|
-
type: "button",
|
|
1137
|
-
size: "sm",
|
|
1138
|
-
className: "bg-brand text-brand-fg hover:bg-brand/90",
|
|
1139
|
-
onClick: () => fileInput.current?.click(),
|
|
1140
|
-
children: labels.imagePickerBrowse
|
|
1141
|
-
}
|
|
1142
|
-
)
|
|
1143
|
-
] }),
|
|
1144
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1145
|
-
"input",
|
|
1146
|
-
{
|
|
1147
|
-
ref: fileInput,
|
|
1148
|
-
type: "file",
|
|
1149
|
-
accept: library.accept ?? "image/*",
|
|
1150
|
-
multiple: true,
|
|
1151
|
-
hidden: true,
|
|
1152
|
-
onChange: (e) => {
|
|
1153
|
-
void upload(e.target.files);
|
|
1154
|
-
e.target.value = "";
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
)
|
|
1158
|
-
]
|
|
1159
|
-
}
|
|
1160
|
-
)),
|
|
1161
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1162
|
-
"form",
|
|
1163
|
-
{
|
|
1164
|
-
className: "flex gap-2",
|
|
1165
|
-
onSubmit: (e) => {
|
|
1166
|
-
e.preventDefault();
|
|
1167
|
-
const value = url.trim();
|
|
1168
|
-
if (value) onSelect(value);
|
|
1169
|
-
},
|
|
1170
|
-
children: [
|
|
1171
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative flex-1", children: [
|
|
1172
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
|
|
1173
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1174
|
-
Input,
|
|
1175
|
-
{
|
|
1176
|
-
value: url,
|
|
1177
|
-
onChange: (e) => setUrl(e.target.value),
|
|
1178
|
-
placeholder: labels.imagePickerUrlPlaceholder,
|
|
1179
|
-
className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
|
|
1180
|
-
}
|
|
1181
|
-
)
|
|
1182
|
-
] }),
|
|
1183
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1184
|
-
Button,
|
|
1185
|
-
{
|
|
1186
|
-
type: "submit",
|
|
1187
|
-
size: "lg",
|
|
1188
|
-
variant: "outline",
|
|
1189
|
-
disabled: !url.trim(),
|
|
1190
|
-
className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
|
|
1191
|
-
children: labels.imagePickerUseUrl
|
|
1192
|
-
}
|
|
1193
|
-
)
|
|
1194
|
-
]
|
|
1195
|
-
}
|
|
1196
|
-
),
|
|
1197
|
-
library && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
|
|
1198
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
|
|
1199
|
-
loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
|
|
1200
|
-
Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1201
|
-
"li",
|
|
1172
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1173
|
+
DialogContent,
|
|
1174
|
+
{
|
|
1175
|
+
onDragOver: (e) => e.preventDefault(),
|
|
1176
|
+
onDrop: (e) => e.preventDefault(),
|
|
1177
|
+
className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl",
|
|
1178
|
+
children: [
|
|
1179
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
|
|
1180
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
|
|
1181
|
+
quota && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
|
|
1182
|
+
] }),
|
|
1183
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
|
|
1184
|
+
library?.upload && (full ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "rounded-lg border border-panel-border bg-panel-elevated px-4 py-3 text-sm text-panel-muted-fg", children: labels.imagePickerQuotaFull }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1185
|
+
"div",
|
|
1202
1186
|
{
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1187
|
+
onDragOver: (e) => {
|
|
1188
|
+
e.preventDefault();
|
|
1189
|
+
setDragging(true);
|
|
1190
|
+
},
|
|
1191
|
+
onDragLeave: () => setDragging(false),
|
|
1192
|
+
onDrop,
|
|
1193
|
+
className: cn(
|
|
1194
|
+
"flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
|
|
1195
|
+
dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
|
|
1196
|
+
),
|
|
1197
|
+
children: [
|
|
1198
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
|
|
1199
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
|
|
1200
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: labels.imagePickerDrop }),
|
|
1201
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1202
|
+
Button,
|
|
1203
|
+
{
|
|
1204
|
+
type: "button",
|
|
1205
|
+
size: "sm",
|
|
1206
|
+
className: "bg-brand text-brand-fg hover:bg-brand/90",
|
|
1207
|
+
onClick: () => fileInput.current?.click(),
|
|
1208
|
+
children: labels.imagePickerBrowse
|
|
1209
|
+
}
|
|
1210
|
+
)
|
|
1211
|
+
] }),
|
|
1212
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1213
|
+
"input",
|
|
1214
|
+
{
|
|
1215
|
+
ref: fileInput,
|
|
1216
|
+
type: "file",
|
|
1217
|
+
accept: library.accept ?? "image/*",
|
|
1218
|
+
multiple: true,
|
|
1219
|
+
hidden: true,
|
|
1220
|
+
onChange: (e) => {
|
|
1221
|
+
void upload(e.target.files);
|
|
1222
|
+
e.target.value = "";
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
)
|
|
1226
|
+
]
|
|
1227
|
+
}
|
|
1207
1228
|
)),
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1223
|
-
"img",
|
|
1229
|
+
library?.allowUrl !== false && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1230
|
+
"form",
|
|
1231
|
+
{
|
|
1232
|
+
className: "flex gap-2",
|
|
1233
|
+
onSubmit: (e) => {
|
|
1234
|
+
e.preventDefault();
|
|
1235
|
+
const value = url.trim();
|
|
1236
|
+
if (value) onSelect(value);
|
|
1237
|
+
},
|
|
1238
|
+
children: [
|
|
1239
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative flex-1", children: [
|
|
1240
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
|
|
1241
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1242
|
+
Input,
|
|
1224
1243
|
{
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
className: "
|
|
1244
|
+
value: url,
|
|
1245
|
+
onChange: (e) => setUrl(e.target.value),
|
|
1246
|
+
placeholder: labels.imagePickerUrlPlaceholder,
|
|
1247
|
+
className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
|
|
1229
1248
|
}
|
|
1230
1249
|
)
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1250
|
+
] }),
|
|
1251
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1252
|
+
Button,
|
|
1253
|
+
{
|
|
1254
|
+
type: "submit",
|
|
1255
|
+
size: "lg",
|
|
1256
|
+
variant: "outline",
|
|
1257
|
+
disabled: !url.trim(),
|
|
1258
|
+
className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
|
|
1259
|
+
children: labels.imagePickerUseUrl
|
|
1260
|
+
}
|
|
1261
|
+
)
|
|
1262
|
+
]
|
|
1263
|
+
}
|
|
1264
|
+
),
|
|
1265
|
+
library && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
|
|
1266
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
|
|
1267
|
+
loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
|
|
1268
|
+
Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1269
|
+
"li",
|
|
1237
1270
|
{
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1271
|
+
className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
|
|
1272
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {})
|
|
1273
|
+
},
|
|
1274
|
+
`pending-${i}`
|
|
1275
|
+
)),
|
|
1276
|
+
images.map((image) => {
|
|
1277
|
+
const selected = image.url === currentUrl;
|
|
1278
|
+
const confirming = confirmId === image.id;
|
|
1279
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "group relative", children: [
|
|
1280
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1281
|
+
"button",
|
|
1282
|
+
{
|
|
1283
|
+
type: "button",
|
|
1284
|
+
onClick: () => onSelect(image.url),
|
|
1285
|
+
title: image.name,
|
|
1286
|
+
className: cn(
|
|
1287
|
+
"block aspect-[4/3] w-full overflow-hidden rounded-lg border bg-black/25 transition outline-none focus-visible:ring-2 focus-visible:ring-brand",
|
|
1288
|
+
selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
|
|
1289
|
+
),
|
|
1290
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1291
|
+
"img",
|
|
1292
|
+
{
|
|
1293
|
+
src: image.url,
|
|
1294
|
+
alt: image.name ?? "",
|
|
1295
|
+
loading: "lazy",
|
|
1296
|
+
className: "size-full object-contain"
|
|
1297
|
+
}
|
|
1298
|
+
)
|
|
1299
|
+
}
|
|
1246
1300
|
),
|
|
1247
|
-
children:
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1301
|
+
selected && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute top-1.5 left-1.5 flex size-5 items-center justify-center rounded-full bg-brand text-brand-fg", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
|
|
1302
|
+
image.name && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute inset-x-0 bottom-0 truncate rounded-b-lg bg-black/60 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover:opacity-100", children: image.name }),
|
|
1303
|
+
library.remove && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1304
|
+
"button",
|
|
1305
|
+
{
|
|
1306
|
+
type: "button",
|
|
1307
|
+
onClick: () => void remove(image),
|
|
1308
|
+
onBlur: () => confirming && setConfirmId(null),
|
|
1309
|
+
title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
|
|
1310
|
+
"aria-label": labels.imagePickerDelete,
|
|
1311
|
+
className: cn(
|
|
1312
|
+
"absolute top-1.5 right-1.5 flex h-6 items-center gap-1 rounded-md px-1.5 text-xs text-white transition focus-visible:opacity-100",
|
|
1313
|
+
confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
|
|
1314
|
+
),
|
|
1315
|
+
children: [
|
|
1316
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
|
|
1317
|
+
confirming && labels.imagePickerConfirmDelete
|
|
1318
|
+
]
|
|
1319
|
+
}
|
|
1320
|
+
)
|
|
1321
|
+
] }, image.id);
|
|
1322
|
+
})
|
|
1323
|
+
] })
|
|
1324
|
+
] })
|
|
1255
1325
|
] })
|
|
1256
|
-
]
|
|
1257
|
-
|
|
1258
|
-
|
|
1326
|
+
]
|
|
1327
|
+
}
|
|
1328
|
+
) });
|
|
1259
1329
|
}
|
|
1260
1330
|
function matchesAccept(file, accept) {
|
|
1261
1331
|
if (!accept) return file.type.startsWith("image/");
|
|
@@ -1282,6 +1352,7 @@ var import_react5 = require("react");
|
|
|
1282
1352
|
var import_lucide_react5 = require("lucide-react");
|
|
1283
1353
|
|
|
1284
1354
|
// src/components/editor/inspector/values.ts
|
|
1355
|
+
var import_core2 = require("@mjml-agent-editor/core");
|
|
1285
1356
|
var SIDES = ["top", "right", "bottom", "left"];
|
|
1286
1357
|
function expandBox(value) {
|
|
1287
1358
|
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1312,15 +1383,6 @@ function parseNetwork(name) {
|
|
|
1312
1383
|
function networkName(network, share) {
|
|
1313
1384
|
return share || network === "custom" ? network : `${network}${NO_SHARE}`;
|
|
1314
1385
|
}
|
|
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
1386
|
|
|
1325
1387
|
// src/components/editor/inspector/controls.tsx
|
|
1326
1388
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
@@ -2126,6 +2188,23 @@ function withoutEditorProps(mjml) {
|
|
|
2126
2188
|
)
|
|
2127
2189
|
);
|
|
2128
2190
|
}
|
|
2191
|
+
function withClosedTags(mjml) {
|
|
2192
|
+
return mjml.replace(START_TAG, (tag) => {
|
|
2193
|
+
const selfClosed = /^<(mj-[\w-]+)([\s\S]*?)\s*\/>$/i.exec(tag);
|
|
2194
|
+
return selfClosed ? `<${selfClosed[1]}${selfClosed[2]}></${selfClosed[1]}>` : tag;
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
var TABLE = /<!--[\s\S]*?(?:-->|$)|(<mj-table\b(?:[^>"']|"[^"]*"|'[^']*')*>)((?:<!--[\s\S]*?-->|(?!<!--)[\s\S])*?)(<\/mj-table\s*>)/gi;
|
|
2198
|
+
function withTableRowsKept(mjml) {
|
|
2199
|
+
return mjml.replace(
|
|
2200
|
+
TABLE,
|
|
2201
|
+
(match, open, rows, close) => open ? `${open}<template>${rows}</template>${close}` : match
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
function sourceForEditor(mjml) {
|
|
2205
|
+
const cleaned = withoutEditorProps(mjml);
|
|
2206
|
+
return { mjml: withTableRowsKept(withClosedTags(cleaned)), repaired: cleaned !== mjml };
|
|
2207
|
+
}
|
|
2129
2208
|
function illegalAttributes(tag, attributes) {
|
|
2130
2209
|
const allowed = ALLOWED_ATTRIBUTES[tag];
|
|
2131
2210
|
if (!allowed) return [];
|
|
@@ -3002,7 +3081,10 @@ var ICONS = {
|
|
|
3002
3081
|
"mj-navbar-link": svg(
|
|
3003
3082
|
'<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
3083
|
),
|
|
3005
|
-
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>')
|
|
3084
|
+
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>'),
|
|
3085
|
+
"mj-table": svg(
|
|
3086
|
+
'<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/>'
|
|
3087
|
+
)
|
|
3006
3088
|
};
|
|
3007
3089
|
function tagOf(c) {
|
|
3008
3090
|
return String(c.get("tagName") || c.get("type") || "");
|
|
@@ -3465,12 +3547,12 @@ function ElementSettings({
|
|
|
3465
3547
|
};
|
|
3466
3548
|
const safeLink = (attr) => (link) => {
|
|
3467
3549
|
if (tag === "mj-navbar-link" && attr === "href") {
|
|
3468
|
-
return isSafeNavbarLink(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
|
|
3550
|
+
return (0, import_core2.isSafeNavbarLink)(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
|
|
3469
3551
|
}
|
|
3470
3552
|
if (tag === "mj-navbar" && attr === "base-url") {
|
|
3471
|
-
return component.components().every((c) => isSafeNavbarLink(link, String(c.getAttributes().href ?? "")));
|
|
3553
|
+
return component.components().every((c) => (0, import_core2.isSafeNavbarLink)(link, String(c.getAttributes().href ?? "")));
|
|
3472
3554
|
}
|
|
3473
|
-
return isSafeLink(link);
|
|
3555
|
+
return (0, import_core2.isSafeLink)(link);
|
|
3474
3556
|
};
|
|
3475
3557
|
const control = (f) => {
|
|
3476
3558
|
const value = attributes[f.attr] ?? "";
|
|
@@ -3778,8 +3860,15 @@ function Header({
|
|
|
3778
3860
|
] });
|
|
3779
3861
|
}
|
|
3780
3862
|
|
|
3863
|
+
// src/components/editor/blocks.ts
|
|
3864
|
+
var import_grapesjs = __toESM(require("grapesjs"), 1);
|
|
3865
|
+
var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
|
|
3866
|
+
|
|
3781
3867
|
// src/components/editor/ids.ts
|
|
3782
|
-
var ID_RE = {
|
|
3868
|
+
var ID_RE = {
|
|
3869
|
+
sec: /(?:^|\s)sec-([A-Za-z0-9_-]+)(?=\s|$)/,
|
|
3870
|
+
obj: /(?:^|\s)obj-([A-Za-z0-9_-]+)(?=\s|$)/
|
|
3871
|
+
};
|
|
3783
3872
|
function idOf(c, prefix) {
|
|
3784
3873
|
return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
|
|
3785
3874
|
}
|
|
@@ -3808,19 +3897,292 @@ function freshenCopiedIds(root, added) {
|
|
|
3808
3897
|
const id = idOf(c, prefix);
|
|
3809
3898
|
if (!id || !used(prefix, id, c)) continue;
|
|
3810
3899
|
const cssClass = String(c.getAttributes()["css-class"]);
|
|
3811
|
-
|
|
3900
|
+
const fresh = cssClass.replace(
|
|
3901
|
+
ID_RE[prefix],
|
|
3902
|
+
(token) => token.replace(`${prefix}-${id}`, `${prefix}-${newId()}`)
|
|
3903
|
+
);
|
|
3904
|
+
c.addAttributes({ "css-class": fresh });
|
|
3905
|
+
}
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
// src/components/editor/insert-block.ts
|
|
3910
|
+
var addsBlock = (e) => (e.key === "Enter" || e.key === " ") && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
3911
|
+
var tileAttributes = (id) => ({ tabindex: "0", role: "button", "data-block": id });
|
|
3912
|
+
var tagOf5 = (c) => String(c.get("tagName") ?? "");
|
|
3913
|
+
function closest(c, tag) {
|
|
3914
|
+
for (let cur = c; cur; cur = cur.parent() ?? void 0) if (tagOf5(cur) === tag) return cur;
|
|
3915
|
+
return void 0;
|
|
3916
|
+
}
|
|
3917
|
+
function childOf(parent, c) {
|
|
3918
|
+
let cur = c;
|
|
3919
|
+
while (cur.parent() && cur.parent() !== parent) cur = cur.parent();
|
|
3920
|
+
return cur;
|
|
3921
|
+
}
|
|
3922
|
+
function insertBlock(editor, block) {
|
|
3923
|
+
const content = block.getContent();
|
|
3924
|
+
let body;
|
|
3925
|
+
editor.getWrapper()?.onAll((c) => {
|
|
3926
|
+
if (!body && tagOf5(c) === "mj-body") body = c;
|
|
3927
|
+
});
|
|
3928
|
+
if (typeof content !== "string" || !body) return void 0;
|
|
3929
|
+
const selected = editor.getSelected();
|
|
3930
|
+
const section = closest(selected, "mj-section");
|
|
3931
|
+
const sections = section?.parent() ?? body;
|
|
3932
|
+
const afterSection = section ? { at: section.index() + 1 } : {};
|
|
3933
|
+
const column = closest(selected, "mj-column");
|
|
3934
|
+
let added;
|
|
3935
|
+
if (/^\s*<mj-section\b/i.test(content)) {
|
|
3936
|
+
added = sections.append(content, afterSection)[0];
|
|
3937
|
+
} else if (column && selected) {
|
|
3938
|
+
const at = selected === column ? {} : { at: childOf(column, selected).index() + 1 };
|
|
3939
|
+
added = column.append(content, at)[0];
|
|
3940
|
+
} else {
|
|
3941
|
+
const [wrapper] = sections.append(
|
|
3942
|
+
`<mj-section><mj-column>${content}</mj-column></mj-section>`,
|
|
3943
|
+
afterSection
|
|
3944
|
+
);
|
|
3945
|
+
added = wrapper?.components().at(0)?.components().at(0);
|
|
3946
|
+
}
|
|
3947
|
+
if (!added) return void 0;
|
|
3948
|
+
editor.select(added);
|
|
3949
|
+
if (block.get("activate")) added.getView()?.onActive(void 0);
|
|
3950
|
+
return added;
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
// src/components/editor/blocks.ts
|
|
3954
|
+
var EDITABLE_TAGS = /* @__PURE__ */ new Set([
|
|
3955
|
+
"mj-section",
|
|
3956
|
+
"mj-group",
|
|
3957
|
+
"mj-column",
|
|
3958
|
+
"mj-text",
|
|
3959
|
+
"mj-image",
|
|
3960
|
+
"mj-button",
|
|
3961
|
+
"mj-divider",
|
|
3962
|
+
"mj-spacer",
|
|
3963
|
+
"mj-social",
|
|
3964
|
+
"mj-social-element",
|
|
3965
|
+
"mj-navbar",
|
|
3966
|
+
"mj-navbar-link"
|
|
3967
|
+
]);
|
|
3968
|
+
var CONTENT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-social-element", "mj-navbar-link"]);
|
|
3969
|
+
var EDITOR_INTERNALS = /data-gjs-|__p\b|custom-name/i;
|
|
3970
|
+
var DROPPED_MARKUP = /<(?:script|style|link|meta|iframe|object|embed|form)\b/i;
|
|
3971
|
+
var INLINE_IMAGE = /\b(?:src|srcset|background-url|icon)\s*=\s*(?:"[^"]*data:|'[^']*data:|data:)/i;
|
|
3972
|
+
var LINK_IN_MARKUP = /[\s/"'](?:xlink:)?(?:href|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
3973
|
+
var validator;
|
|
3974
|
+
function blockProblems(mjml, editor = validator ??= import_grapesjs.default.init({
|
|
3975
|
+
headless: true,
|
|
3976
|
+
storageManager: false,
|
|
3977
|
+
telemetry: false,
|
|
3978
|
+
plugins: [import_grapesjs_mjml.default]
|
|
3979
|
+
})) {
|
|
3980
|
+
if (EDITOR_INTERNALS.test(mjml)) return ["it contains GrapesJS's own attributes"];
|
|
3981
|
+
if (INLINE_IMAGE.test(mjml)) return ["it inlines an image as a data: URL"];
|
|
3982
|
+
let tree;
|
|
3983
|
+
try {
|
|
3984
|
+
tree = editor.runCommand("mjml-code-to-html", {
|
|
3985
|
+
mjml: `<mjml><mj-body>${withClosedTags(mjml)}</mj-body></mjml>`,
|
|
3986
|
+
validationLevel: "strict"
|
|
3987
|
+
}).json;
|
|
3988
|
+
} catch (e) {
|
|
3989
|
+
const errors = e.errors;
|
|
3990
|
+
if (!errors) throw e;
|
|
3991
|
+
return errors.map((error) => error.formattedMessage ?? error.message ?? String(error));
|
|
3992
|
+
}
|
|
3993
|
+
const problems = [];
|
|
3994
|
+
const top = tree.children?.find((c) => c.tagName === "mj-body")?.children ?? [];
|
|
3995
|
+
if (top.length !== 1 || top[0].tagName !== "mj-section") {
|
|
3996
|
+
problems.push("it is not exactly one mj-section");
|
|
3997
|
+
}
|
|
3998
|
+
const visit = (node, parent) => {
|
|
3999
|
+
const tag = node.tagName;
|
|
4000
|
+
const attributes = node.attributes ?? {};
|
|
4001
|
+
const content = node.content ?? "";
|
|
4002
|
+
if (!EDITABLE_TAGS.has(tag)) problems.push(`${tag} is not editable here`);
|
|
4003
|
+
const cssClass = attributes["css-class"] ?? "";
|
|
4004
|
+
if (ID_RE.sec.test(cssClass) || ID_RE.obj.test(cssClass)) {
|
|
4005
|
+
problems.push(`${tag} carries a sec-* or obj-* id`);
|
|
4006
|
+
}
|
|
4007
|
+
if ("mj-class" in attributes) problems.push(`${tag} uses mj-class`);
|
|
4008
|
+
const links = [attributes.href ?? ""];
|
|
4009
|
+
for (const match of content.matchAll(LINK_IN_MARKUP)) {
|
|
4010
|
+
links.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
4011
|
+
}
|
|
4012
|
+
const linked = tag === "mj-navbar-link" ? (0, import_core2.isSafeNavbarLink)(parent?.attributes?.["base-url"] ?? "", attributes.href ?? "") : true;
|
|
4013
|
+
if (!linked || !links.every(import_core2.isSafeLink)) {
|
|
4014
|
+
problems.push(`${tag} has a link scheme mail must not carry`);
|
|
4015
|
+
}
|
|
4016
|
+
if (CONTENT_TAGS.has(tag) ? /<mj-/i.test(content) || DROPPED_MARKUP.test(content) : content.trim()) {
|
|
4017
|
+
problems.push(`${tag} holds content the editor does not keep`);
|
|
4018
|
+
}
|
|
4019
|
+
node.children?.forEach((child) => visit(child, node));
|
|
4020
|
+
};
|
|
4021
|
+
top.forEach((node) => visit(node));
|
|
4022
|
+
return problems;
|
|
4023
|
+
}
|
|
4024
|
+
var escapeHtml3 = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4025
|
+
function registerBlocks(editor, blocks, category, media) {
|
|
4026
|
+
for (const block of blocks) {
|
|
4027
|
+
const problems = editor.Blocks.get(block.id) ? [`the id is already taken`] : blockProblems(block.mjml, editor);
|
|
4028
|
+
if (problems.length > 0) {
|
|
4029
|
+
console.error(`Block "${block.id}" is left out of the palette: ${problems.join("; ")}`);
|
|
4030
|
+
continue;
|
|
4031
|
+
}
|
|
4032
|
+
editor.Blocks.add(block.id, {
|
|
4033
|
+
// GrapesJS writes the label into the tile as markup.
|
|
4034
|
+
label: escapeHtml3(block.label),
|
|
4035
|
+
category,
|
|
4036
|
+
media: block.media ?? media,
|
|
4037
|
+
content: withClosedTags(block.mjml),
|
|
4038
|
+
attributes: tileAttributes(block.id)
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
// src/components/editor/builtin-blocks.ts
|
|
4044
|
+
var BUILTIN_BLOCKS = [
|
|
4045
|
+
"mj-1-column",
|
|
4046
|
+
"mj-2-columns",
|
|
4047
|
+
"mj-3-columns",
|
|
4048
|
+
"mj-text",
|
|
4049
|
+
"mj-image",
|
|
4050
|
+
"mj-button",
|
|
4051
|
+
"mj-divider",
|
|
4052
|
+
"mj-spacer",
|
|
4053
|
+
"mj-social-group",
|
|
4054
|
+
"mj-navbar"
|
|
4055
|
+
];
|
|
4056
|
+
var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
|
|
4057
|
+
var CONTENT2 = {
|
|
4058
|
+
// The plugin's version links Google+ and shares the page: a bare network name makes the
|
|
4059
|
+
// icon open a "share this link" dialog, while `-noshare` links to the profile a footer
|
|
4060
|
+
// means. Without `alt` each link is an unnamed image to a screen reader.
|
|
4061
|
+
"mj-social-group": `<mj-social icon-size="24px" mode="horizontal">
|
|
4062
|
+
<mj-social-element name="facebook-noshare" href="https://facebook.com/" alt="Facebook"></mj-social-element>
|
|
4063
|
+
<mj-social-element name="instagram-noshare" href="https://instagram.com/" alt="Instagram"></mj-social-element>
|
|
4064
|
+
<mj-social-element name="x-noshare" href="https://x.com/" alt="X"></mj-social-element>
|
|
4065
|
+
</mj-social>`,
|
|
4066
|
+
// The plugin's sample is a via.placeholder.com picture, stored in the email and loaded by
|
|
4067
|
+
// every reader's mail client. Dropping one opens the image picker instead.
|
|
4068
|
+
"mj-image": "<mj-image></mj-image>"
|
|
4069
|
+
};
|
|
4070
|
+
function builtinBlock(labels) {
|
|
4071
|
+
return (id) => ({
|
|
4072
|
+
label: labels.blockLabels[id] ?? id,
|
|
4073
|
+
// By id: a regex on the label broke as soon as a host translated it.
|
|
4074
|
+
category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
|
|
4075
|
+
...id in CONTENT2 && { content: CONTENT2[id] },
|
|
4076
|
+
attributes: tileAttributes(id)
|
|
4077
|
+
});
|
|
4078
|
+
}
|
|
4079
|
+
var CANVAS_CSS = 'img:not([src]), img[src=""] { min-height: 120px; background: #f4f4f5; outline: 1px dashed #a1a1aa; outline-offset: -1px; }';
|
|
4080
|
+
|
|
4081
|
+
// src/components/editor/table-type.ts
|
|
4082
|
+
var import_core3 = require("@mjml-agent-editor/core");
|
|
4083
|
+
var DROPPED = "script, style, link, iframe, frame, object, embed, applet, base, meta, animate, set, svg, math, template";
|
|
4084
|
+
var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href", "action", "formaction"]);
|
|
4085
|
+
function cleanRows(rows) {
|
|
4086
|
+
rows.querySelectorAll(DROPPED).forEach((el) => el.remove());
|
|
4087
|
+
rows.querySelectorAll("*").forEach((el) => {
|
|
4088
|
+
for (const { name, value } of [...el.attributes]) {
|
|
4089
|
+
const lower = name.toLowerCase();
|
|
4090
|
+
const script = /^(java|vb)script:/i.test(value.replace(/[\u0000-\u0020]/g, ""));
|
|
4091
|
+
if (lower.startsWith("on") || script || LINK_ATTRIBUTES.has(lower) && !(0, import_core3.isSafeLink)(value)) {
|
|
4092
|
+
el.removeAttribute(name);
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
});
|
|
4096
|
+
}
|
|
4097
|
+
var ROW_STYLE = "pointer-events: all; display: table; width: 100%;";
|
|
4098
|
+
function tablePreview(doc, attributes, rows) {
|
|
4099
|
+
const attr = (name, fallback) => String(attributes[name] ?? fallback);
|
|
4100
|
+
const template = doc.createElement("template");
|
|
4101
|
+
template.innerHTML = `<table>${rows}</table>`;
|
|
4102
|
+
const table = doc.createElement("table");
|
|
4103
|
+
table.setAttribute("cellpadding", String(parseInt(attr("cellpadding", "0"), 10) || 0));
|
|
4104
|
+
table.setAttribute("cellspacing", String(parseInt(attr("cellspacing", "0"), 10) || 0));
|
|
4105
|
+
Object.assign(table.style, {
|
|
4106
|
+
width: attr("width", "100%"),
|
|
4107
|
+
border: attr("border", "none"),
|
|
4108
|
+
color: attr("color", "#000000"),
|
|
4109
|
+
fontFamily: attr("font-family", "Ubuntu, Helvetica, Arial, sans-serif"),
|
|
4110
|
+
fontSize: attr("font-size", "13px"),
|
|
4111
|
+
lineHeight: attr("line-height", "22px"),
|
|
4112
|
+
tableLayout: attr("table-layout", "auto")
|
|
4113
|
+
});
|
|
4114
|
+
for (const row of template.content.querySelectorAll("tr")) {
|
|
4115
|
+
const tr = table.insertRow();
|
|
4116
|
+
for (const cell2 of row.querySelectorAll(":scope > th, :scope > td")) {
|
|
4117
|
+
const copy = doc.createElement(cell2.tagName.toLowerCase());
|
|
4118
|
+
copy.textContent = cell2.textContent;
|
|
4119
|
+
const align = cell2.getAttribute("align");
|
|
4120
|
+
if (align && /^(left|right|center)$/i.test(align)) copy.setAttribute("align", align);
|
|
4121
|
+
tr.append(copy);
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
const cell = doc.createElement("td");
|
|
4125
|
+
cell.style.padding = attr("padding", "10px 25px");
|
|
4126
|
+
cell.append(table);
|
|
4127
|
+
return cell;
|
|
4128
|
+
}
|
|
4129
|
+
function addTableType(editor) {
|
|
4130
|
+
const base = editor.Components.getType("default").view.prototype;
|
|
4131
|
+
editor.Components.addType("mj-table", {
|
|
4132
|
+
isComponent: (el) => {
|
|
4133
|
+
if (el.tagName?.toLowerCase() !== "mj-table") return false;
|
|
4134
|
+
const rows = el.querySelector(":scope > template");
|
|
4135
|
+
if (rows) cleanRows(rows.content);
|
|
4136
|
+
return { type: "mj-table", content: rows?.innerHTML ?? "", components: [] };
|
|
4137
|
+
},
|
|
4138
|
+
model: { defaults: { tagName: "mj-table", droppable: false, editable: false } },
|
|
4139
|
+
view: {
|
|
4140
|
+
tagName: "tr",
|
|
4141
|
+
// GrapesJS clears the element's attributes, style among them, when the model's change.
|
|
4142
|
+
updateAttributes(...args) {
|
|
4143
|
+
base.updateAttributes.apply(this, args);
|
|
4144
|
+
this.el.style.cssText = ROW_STYLE;
|
|
4145
|
+
},
|
|
4146
|
+
updateContent() {
|
|
4147
|
+
const { el, model } = this;
|
|
4148
|
+
el.style.cssText = ROW_STYLE;
|
|
4149
|
+
el.replaceChildren(
|
|
4150
|
+
tablePreview(el.ownerDocument, model.getAttributes(), String(model.get("content") ?? ""))
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
3812
4153
|
}
|
|
3813
4154
|
});
|
|
3814
4155
|
}
|
|
3815
4156
|
|
|
3816
4157
|
// src/components/editor/plugin-types.ts
|
|
4158
|
+
var COLUMN_CHILDREN = /* @__PURE__ */ new Set([
|
|
4159
|
+
"mj-accordion",
|
|
4160
|
+
"mj-button",
|
|
4161
|
+
"mj-carousel",
|
|
4162
|
+
"mj-divider",
|
|
4163
|
+
"mj-image",
|
|
4164
|
+
"mj-navbar",
|
|
4165
|
+
"mj-raw",
|
|
4166
|
+
"mj-social",
|
|
4167
|
+
"mj-spacer",
|
|
4168
|
+
"mj-table",
|
|
4169
|
+
"mj-text"
|
|
4170
|
+
]);
|
|
3817
4171
|
function fixPluginTypes(editor) {
|
|
4172
|
+
addTableType(editor);
|
|
3818
4173
|
for (const type of editor.Components.getTypes()) {
|
|
3819
|
-
if (!type.id.startsWith("mj-")) continue;
|
|
4174
|
+
if (!type.id.startsWith("mj-") || type.id === "mj-table") continue;
|
|
3820
4175
|
const base = type.model.prototype.getAttrToHTML;
|
|
3821
4176
|
editor.Components.addType(type.id, {
|
|
3822
4177
|
model: {
|
|
3823
|
-
defaults: {
|
|
4178
|
+
defaults: {
|
|
4179
|
+
"style-default": {},
|
|
4180
|
+
...type.id === "mj-column" && {
|
|
4181
|
+
droppable: (source) => COLUMN_CHILDREN.has(String(source.get("tagName")))
|
|
4182
|
+
},
|
|
4183
|
+
// GrapesJS's own is an SVG's markup, which the canvas wrote into `src` and saved.
|
|
4184
|
+
...type.id === "mj-image" && { src: "" }
|
|
4185
|
+
},
|
|
3824
4186
|
getAttrToHTML() {
|
|
3825
4187
|
const attributes = base.call(this);
|
|
3826
4188
|
delete attributes.__p;
|
|
@@ -3842,16 +4204,17 @@ var TYPE_LABEL = {
|
|
|
3842
4204
|
"mj-image": "Image",
|
|
3843
4205
|
"mj-column": "Column"
|
|
3844
4206
|
};
|
|
3845
|
-
function
|
|
4207
|
+
function tagOf6(c) {
|
|
3846
4208
|
return String(c.get("tagName") || c.get("type") || "");
|
|
3847
4209
|
}
|
|
3848
4210
|
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
4211
|
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>';
|
|
4212
|
+
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
4213
|
function isSection(c) {
|
|
3851
|
-
return
|
|
4214
|
+
return tagOf6(c) === "mj-section";
|
|
3852
4215
|
}
|
|
3853
4216
|
function isCommentable(c) {
|
|
3854
|
-
const t =
|
|
4217
|
+
const t = tagOf6(c);
|
|
3855
4218
|
return t === "mj-section" || t in TYPE_LABEL;
|
|
3856
4219
|
}
|
|
3857
4220
|
function closestSection(c) {
|
|
@@ -3863,29 +4226,11 @@ function closestSection(c) {
|
|
|
3863
4226
|
return null;
|
|
3864
4227
|
}
|
|
3865
4228
|
function crumbName(c, labels) {
|
|
3866
|
-
const t =
|
|
4229
|
+
const t = tagOf6(c);
|
|
3867
4230
|
return labels.elementNames[t] ?? t.replace(/^mj-/, "");
|
|
3868
4231
|
}
|
|
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
4232
|
function objectLabel(c) {
|
|
3888
|
-
const base = TYPE_LABEL[
|
|
4233
|
+
const base = TYPE_LABEL[tagOf6(c)] ?? tagOf6(c);
|
|
3889
4234
|
const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
|
|
3890
4235
|
return text ? `${base}: \u201C${text}\u201D` : base;
|
|
3891
4236
|
}
|
|
@@ -3953,7 +4298,13 @@ function setupRichText(editor, labels) {
|
|
|
3953
4298
|
result: (r, action) => r.exec("hiliteColor", val(action, "input"))
|
|
3954
4299
|
});
|
|
3955
4300
|
}
|
|
3956
|
-
function EmailEditor({
|
|
4301
|
+
function EmailEditor({
|
|
4302
|
+
docId,
|
|
4303
|
+
onReady,
|
|
4304
|
+
commentsRefresh,
|
|
4305
|
+
onOpenCountChange,
|
|
4306
|
+
blocks = []
|
|
4307
|
+
}) {
|
|
3957
4308
|
const documents = useDocumentStore();
|
|
3958
4309
|
const labels = useLabels();
|
|
3959
4310
|
const loadingRef = (0, import_react10.useRef)(false);
|
|
@@ -3975,8 +4326,15 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3975
4326
|
const readOnlyRef = (0, import_react10.useRef)(false);
|
|
3976
4327
|
const applyReadOnly = (0, import_react10.useRef)(async () => {
|
|
3977
4328
|
});
|
|
3978
|
-
(0, import_react10.
|
|
3979
|
-
(0, import_react10.useEffect)(() => ()
|
|
4329
|
+
const [turn] = (0, import_react10.useState)(createTurnFollower);
|
|
4330
|
+
(0, import_react10.useEffect)(() => turn.turn(turnRunning), [turn, turnRunning]);
|
|
4331
|
+
(0, import_react10.useEffect)(() => {
|
|
4332
|
+
turn.mount();
|
|
4333
|
+
return () => {
|
|
4334
|
+
turn.unmount();
|
|
4335
|
+
setLocked(false);
|
|
4336
|
+
};
|
|
4337
|
+
}, [turn, setLocked]);
|
|
3980
4338
|
const [crumbs, setCrumbs] = (0, import_react10.useState)([]);
|
|
3981
4339
|
const [picker, setPicker] = (0, import_react10.useState)(
|
|
3982
4340
|
null
|
|
@@ -3987,7 +4345,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3987
4345
|
const chain = [];
|
|
3988
4346
|
let cur = c;
|
|
3989
4347
|
while (cur) {
|
|
3990
|
-
const t =
|
|
4348
|
+
const t = tagOf6(cur);
|
|
3991
4349
|
if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
|
|
3992
4350
|
chain.unshift(cur);
|
|
3993
4351
|
cur = cur.parent() ?? void 0;
|
|
@@ -4036,7 +4394,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4036
4394
|
if (isSection(c)) addIdClass(c, "sec");
|
|
4037
4395
|
if (!isCommentable(c)) return;
|
|
4038
4396
|
const toolbar = [...c.get("toolbar") ?? []];
|
|
4039
|
-
if (
|
|
4397
|
+
if (tagOf6(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
|
|
4040
4398
|
toolbar.push({
|
|
4041
4399
|
command: "open-image-picker",
|
|
4042
4400
|
label: IMAGE_SVG,
|
|
@@ -4056,7 +4414,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4056
4414
|
editor.Commands.add("open-image-picker", {
|
|
4057
4415
|
run(ed) {
|
|
4058
4416
|
const target = ed.getSelected();
|
|
4059
|
-
if (!target ||
|
|
4417
|
+
if (!target || tagOf6(target) !== "mj-image") return;
|
|
4060
4418
|
const am = ed.AssetManager;
|
|
4061
4419
|
am.open({
|
|
4062
4420
|
target,
|
|
@@ -4076,6 +4434,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4076
4434
|
});
|
|
4077
4435
|
setupRichText(editor, labels);
|
|
4078
4436
|
fixPluginTypes(editor);
|
|
4437
|
+
registerBlocks(editor, blocks, labels.blockCategorySections, SECTION_SVG);
|
|
4079
4438
|
setupLayerNames(editor, labels);
|
|
4080
4439
|
const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
|
|
4081
4440
|
const showBodyInLayers = () => {
|
|
@@ -4083,14 +4442,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4083
4442
|
if (!body) return;
|
|
4084
4443
|
editor.Layers.setRoot(body);
|
|
4085
4444
|
body.onAll((c) => {
|
|
4086
|
-
if (STRUCTURE.has(
|
|
4445
|
+
if (STRUCTURE.has(tagOf6(c))) editor.Layers.setOpen(c, true);
|
|
4087
4446
|
});
|
|
4088
4447
|
};
|
|
4089
4448
|
const keepStructureOpen = () => {
|
|
4090
4449
|
const layers = editor.Layers;
|
|
4091
4450
|
const opened = layers.model?.get("opened") ?? {};
|
|
4092
4451
|
for (const [cid, c] of Object.entries(opened)) {
|
|
4093
|
-
if (STRUCTURE.has(
|
|
4452
|
+
if (STRUCTURE.has(tagOf6(c))) delete opened[cid];
|
|
4094
4453
|
}
|
|
4095
4454
|
};
|
|
4096
4455
|
editor.on("component:add", (c) => {
|
|
@@ -4118,16 +4477,16 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4118
4477
|
editor.on("component:deselected", () => {
|
|
4119
4478
|
buildCrumbs(editor.getSelected());
|
|
4120
4479
|
});
|
|
4121
|
-
|
|
4480
|
+
const baseline = createSaveBaseline();
|
|
4122
4481
|
const save = async () => {
|
|
4123
4482
|
const view = editor.getEditing()?.getView();
|
|
4124
4483
|
await view?.syncContent?.();
|
|
4125
4484
|
const mjml = editor.getHtml();
|
|
4126
|
-
if (mjml
|
|
4485
|
+
if (!baseline.changed(mjml)) return;
|
|
4127
4486
|
setSave("saving");
|
|
4128
4487
|
try {
|
|
4129
4488
|
await documents.save(docId, { mjml, projectData: editor.getProjectData() });
|
|
4130
|
-
|
|
4489
|
+
baseline.saved(mjml);
|
|
4131
4490
|
setSave("saved");
|
|
4132
4491
|
} catch (e) {
|
|
4133
4492
|
setSave("error");
|
|
@@ -4158,6 +4517,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4158
4517
|
});
|
|
4159
4518
|
}
|
|
4160
4519
|
const lock = (on) => {
|
|
4520
|
+
if (!turn.mounted) return;
|
|
4161
4521
|
readOnlyRef.current = on;
|
|
4162
4522
|
setLocked(on);
|
|
4163
4523
|
notifyState();
|
|
@@ -4183,37 +4543,36 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4183
4543
|
return;
|
|
4184
4544
|
}
|
|
4185
4545
|
lock(false);
|
|
4546
|
+
queue.schedule();
|
|
4186
4547
|
};
|
|
4187
4548
|
const reload = () => queue.run(async () => {
|
|
4188
4549
|
const fresh = await documents.get(docId);
|
|
4189
|
-
if (fresh.mjml !== stored) loadMjml(fresh.mjml);
|
|
4550
|
+
if (fresh.mjml !== baseline.stored) loadMjml(fresh.mjml);
|
|
4190
4551
|
});
|
|
4191
4552
|
const loadMjml = (mjml) => {
|
|
4192
4553
|
loadingRef.current = true;
|
|
4193
4554
|
let repaired = false;
|
|
4555
|
+
const adds = !mjml || (0, import_core4.listSections)(mjml).some((s) => !s.sectionId);
|
|
4194
4556
|
try {
|
|
4195
4557
|
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
|
-
}
|
|
4558
|
+
baseline.load(mjml, null);
|
|
4559
|
+
const source = sourceForEditor(mjml || import_core4.STARTER_MJML);
|
|
4560
|
+
repaired = source.repaired;
|
|
4561
|
+
editor.setComponents(source.mjml);
|
|
4204
4562
|
editor.getWrapper()?.onAll((c) => {
|
|
4205
|
-
const illegal = illegalAttributes(
|
|
4563
|
+
const illegal = illegalAttributes(tagOf6(c), c.getAttributes());
|
|
4206
4564
|
if (illegal.length === 0) return;
|
|
4207
4565
|
c.removeAttributes(illegal);
|
|
4208
4566
|
repaired = true;
|
|
4209
4567
|
});
|
|
4568
|
+
if (!repaired && !adds) baseline.load(mjml, editor.getHtml());
|
|
4210
4569
|
showBodyInLayers();
|
|
4211
4570
|
editor.UndoManager.clear();
|
|
4212
4571
|
} finally {
|
|
4213
4572
|
loadingRef.current = false;
|
|
4214
4573
|
}
|
|
4215
4574
|
notifyState();
|
|
4216
|
-
if (repaired && !readOnlyRef.current) queue.schedule();
|
|
4575
|
+
if ((repaired || adds) && !readOnlyRef.current) queue.schedule();
|
|
4217
4576
|
};
|
|
4218
4577
|
const ensureHighlightStyles = () => {
|
|
4219
4578
|
const cdoc = editor.Canvas.getDocument();
|
|
@@ -4235,7 +4594,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4235
4594
|
cdoc.head.appendChild(style);
|
|
4236
4595
|
};
|
|
4237
4596
|
const sectionEl = (sectionId) => {
|
|
4238
|
-
|
|
4597
|
+
let comp;
|
|
4598
|
+
editor.getWrapper()?.onAll((c) => {
|
|
4599
|
+
if (!comp && isSection(c) && sectionIdOf(c) === sectionId) comp = c;
|
|
4600
|
+
});
|
|
4239
4601
|
return comp?.getEl?.() ?? void 0;
|
|
4240
4602
|
};
|
|
4241
4603
|
const compiledHtml = () => {
|
|
@@ -4246,9 +4608,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4246
4608
|
return editor.getHtml();
|
|
4247
4609
|
}
|
|
4248
4610
|
};
|
|
4611
|
+
turn.ready((on) => void applyReadOnly.current(on));
|
|
4249
4612
|
try {
|
|
4250
|
-
|
|
4251
|
-
loadMjml(doc.mjml);
|
|
4613
|
+
await queue.run(async () => loadMjml((await documents.get(docId)).mjml));
|
|
4252
4614
|
} catch (e) {
|
|
4253
4615
|
import_sonner2.toast.error(labels.documentLoadFailed);
|
|
4254
4616
|
console.error(e);
|
|
@@ -4303,7 +4665,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4303
4665
|
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4304
4666
|
import_react9.default,
|
|
4305
4667
|
{
|
|
4306
|
-
grapesjs:
|
|
4668
|
+
grapesjs: import_grapesjs2.default,
|
|
4307
4669
|
className: "h-full",
|
|
4308
4670
|
options: {
|
|
4309
4671
|
height: "100%",
|
|
@@ -4312,6 +4674,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4312
4674
|
// app.grapesjs.com and leave a session-storage entry, which a host would then have
|
|
4313
4675
|
// to disclose and, for the entry, ask consent for.
|
|
4314
4676
|
telemetry: false,
|
|
4677
|
+
canvasCss: CANVAS_CSS,
|
|
4315
4678
|
fromElement: false,
|
|
4316
4679
|
panels: { defaults: [] },
|
|
4317
4680
|
blockManager: { appendTo: "#gjs-blocks" },
|
|
@@ -4331,7 +4694,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4331
4694
|
close: () => setPicker(null)
|
|
4332
4695
|
}
|
|
4333
4696
|
},
|
|
4334
|
-
canvas: {
|
|
4697
|
+
canvas: {
|
|
4698
|
+
customBadgeLabel: badgeLabel(labels),
|
|
4699
|
+
// A file or text dragged in from outside lands as `<img>` or `<div>`, which no MJML
|
|
4700
|
+
// validator accepts; drags from the palette are unaffected.
|
|
4701
|
+
allowExternalDrop: false
|
|
4702
|
+
},
|
|
4335
4703
|
deviceManager: {
|
|
4336
4704
|
devices: [
|
|
4337
4705
|
{ id: "desktop", name: "Desktop", width: "" },
|
|
@@ -4339,14 +4707,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
4339
4707
|
]
|
|
4340
4708
|
},
|
|
4341
4709
|
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
|
-
})
|
|
4710
|
+
(editor) => (0, import_grapesjs_mjml2.default)(editor, {
|
|
4711
|
+
blocks: BUILTIN_BLOCKS,
|
|
4712
|
+
block: builtinBlock(labels)
|
|
4350
4713
|
})
|
|
4351
4714
|
]
|
|
4352
4715
|
},
|
|
@@ -4428,7 +4791,7 @@ function LeftSidebar({
|
|
|
4428
4791
|
const selectBody = () => {
|
|
4429
4792
|
let body;
|
|
4430
4793
|
editor?.getWrapper()?.onAll((c) => {
|
|
4431
|
-
if (!body &&
|
|
4794
|
+
if (!body && tagOf6(c) === "mj-body") body = c;
|
|
4432
4795
|
});
|
|
4433
4796
|
if (body) editor?.select(body);
|
|
4434
4797
|
};
|
|
@@ -4436,6 +4799,13 @@ function LeftSidebar({
|
|
|
4436
4799
|
setLayersExpanded(expand);
|
|
4437
4800
|
editor?.getWrapper()?.onAll((c) => c.set("open", expand));
|
|
4438
4801
|
};
|
|
4802
|
+
const onTileKey = (e) => {
|
|
4803
|
+
const tile = e.target.closest("[data-block]");
|
|
4804
|
+
const block = tile && editor?.Blocks.get(tile.dataset.block ?? "");
|
|
4805
|
+
if (!editor || !block || !addsBlock(e)) return;
|
|
4806
|
+
e.preventDefault();
|
|
4807
|
+
if (insertBlock(editor, block)) (0, import_sonner2.toast)(labels.blockAdded(tile.textContent?.trim() ?? ""));
|
|
4808
|
+
};
|
|
4439
4809
|
const filterBlocks = (q) => {
|
|
4440
4810
|
setBlockSearch(q);
|
|
4441
4811
|
const root = document.getElementById("gjs-blocks");
|
|
@@ -4492,7 +4862,7 @@ function LeftSidebar({
|
|
|
4492
4862
|
/* @__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
4863
|
] }),
|
|
4494
4864
|
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" })
|
|
4865
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: onTileKey })
|
|
4496
4866
|
] }),
|
|
4497
4867
|
/* @__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
4868
|
Inspector,
|
|
@@ -4573,6 +4943,7 @@ function Breadcrumb({
|
|
|
4573
4943
|
}
|
|
4574
4944
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4575
4945
|
0 && (module.exports = {
|
|
4576
|
-
EmailEditor
|
|
4946
|
+
EmailEditor,
|
|
4947
|
+
blockProblems
|
|
4577
4948
|
});
|
|
4578
4949
|
//# sourceMappingURL=canvas.cjs.map
|