@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.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
useDocumentStore,
|
|
17
17
|
useImageLibrary,
|
|
18
18
|
useLabels
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-PXFZVLNQ.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;
|
|
@@ -161,7 +218,8 @@ function ImagePicker({
|
|
|
161
218
|
const accepts = (file) => matchesAccept(file, library?.accept);
|
|
162
219
|
const full = quota !== void 0 && quota.used >= quota.limit;
|
|
163
220
|
const upload = async (files) => {
|
|
164
|
-
|
|
221
|
+
const send = library?.upload?.bind(library);
|
|
222
|
+
if (!send || !files?.length) return;
|
|
165
223
|
const mine = session.current;
|
|
166
224
|
const list = Array.from(files).filter((f) => {
|
|
167
225
|
if (accepts(f)) return true;
|
|
@@ -170,7 +228,7 @@ function ImagePicker({
|
|
|
170
228
|
});
|
|
171
229
|
if (!list.length) return;
|
|
172
230
|
setPending((n) => n + list.length);
|
|
173
|
-
const results = await Promise.allSettled(list.map(async (f) =>
|
|
231
|
+
const results = await Promise.allSettled(list.map(async (f) => send(f)));
|
|
174
232
|
setPending((n) => n - list.length);
|
|
175
233
|
for (const r of results) {
|
|
176
234
|
if (r.status === "rejected") {
|
|
@@ -205,155 +263,163 @@ function ImagePicker({
|
|
|
205
263
|
setDragging(false);
|
|
206
264
|
if (!full) void upload(e.dataTransfer.files);
|
|
207
265
|
};
|
|
208
|
-
return /* @__PURE__ */ jsx2(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxs(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
"
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
onDrop,
|
|
223
|
-
className: cn(
|
|
224
|
-
"flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
|
|
225
|
-
dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
|
|
226
|
-
),
|
|
227
|
-
children: [
|
|
228
|
-
/* @__PURE__ */ jsx2(ImageUp, { className: "size-8 text-panel-muted-fg" }),
|
|
229
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
|
|
230
|
-
/* @__PURE__ */ jsx2("span", { children: labels.imagePickerDrop }),
|
|
231
|
-
/* @__PURE__ */ jsx2(
|
|
232
|
-
Button,
|
|
233
|
-
{
|
|
234
|
-
type: "button",
|
|
235
|
-
size: "sm",
|
|
236
|
-
className: "bg-brand text-brand-fg hover:bg-brand/90",
|
|
237
|
-
onClick: () => fileInput.current?.click(),
|
|
238
|
-
children: labels.imagePickerBrowse
|
|
239
|
-
}
|
|
240
|
-
)
|
|
241
|
-
] }),
|
|
242
|
-
/* @__PURE__ */ jsx2(
|
|
243
|
-
"input",
|
|
244
|
-
{
|
|
245
|
-
ref: fileInput,
|
|
246
|
-
type: "file",
|
|
247
|
-
accept: library.accept ?? "image/*",
|
|
248
|
-
multiple: true,
|
|
249
|
-
hidden: true,
|
|
250
|
-
onChange: (e) => {
|
|
251
|
-
void upload(e.target.files);
|
|
252
|
-
e.target.value = "";
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
)
|
|
256
|
-
]
|
|
257
|
-
}
|
|
258
|
-
)),
|
|
259
|
-
/* @__PURE__ */ jsxs(
|
|
260
|
-
"form",
|
|
261
|
-
{
|
|
262
|
-
className: "flex gap-2",
|
|
263
|
-
onSubmit: (e) => {
|
|
264
|
-
e.preventDefault();
|
|
265
|
-
const value = url.trim();
|
|
266
|
-
if (value) onSelect(value);
|
|
267
|
-
},
|
|
268
|
-
children: [
|
|
269
|
-
/* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
270
|
-
/* @__PURE__ */ jsx2(Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
|
|
271
|
-
/* @__PURE__ */ jsx2(
|
|
272
|
-
Input,
|
|
273
|
-
{
|
|
274
|
-
value: url,
|
|
275
|
-
onChange: (e) => setUrl(e.target.value),
|
|
276
|
-
placeholder: labels.imagePickerUrlPlaceholder,
|
|
277
|
-
className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
|
|
278
|
-
}
|
|
279
|
-
)
|
|
280
|
-
] }),
|
|
281
|
-
/* @__PURE__ */ jsx2(
|
|
282
|
-
Button,
|
|
283
|
-
{
|
|
284
|
-
type: "submit",
|
|
285
|
-
size: "lg",
|
|
286
|
-
variant: "outline",
|
|
287
|
-
disabled: !url.trim(),
|
|
288
|
-
className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
|
|
289
|
-
children: labels.imagePickerUseUrl
|
|
290
|
-
}
|
|
291
|
-
)
|
|
292
|
-
]
|
|
293
|
-
}
|
|
294
|
-
),
|
|
295
|
-
library && /* @__PURE__ */ jsxs("section", { children: [
|
|
296
|
-
/* @__PURE__ */ jsx2("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
|
|
297
|
-
loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ jsx2("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ jsx2(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ jsx2("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ jsxs("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
|
|
298
|
-
Array.from({ length: pending }, (_, i) => /* @__PURE__ */ jsx2(
|
|
299
|
-
"li",
|
|
266
|
+
return /* @__PURE__ */ jsx2(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxs(
|
|
267
|
+
DialogContent,
|
|
268
|
+
{
|
|
269
|
+
onDragOver: (e) => e.preventDefault(),
|
|
270
|
+
onDrop: (e) => e.preventDefault(),
|
|
271
|
+
className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl",
|
|
272
|
+
children: [
|
|
273
|
+
/* @__PURE__ */ jsxs(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
|
|
274
|
+
/* @__PURE__ */ jsx2(DialogTitle, { children: labels.imagePickerTitle }),
|
|
275
|
+
quota && /* @__PURE__ */ jsx2(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
|
|
276
|
+
] }),
|
|
277
|
+
/* @__PURE__ */ jsxs("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
|
|
278
|
+
library?.upload && (full ? /* @__PURE__ */ jsx2("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__ */ jsxs(
|
|
279
|
+
"div",
|
|
300
280
|
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
281
|
+
onDragOver: (e) => {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
setDragging(true);
|
|
284
|
+
},
|
|
285
|
+
onDragLeave: () => setDragging(false),
|
|
286
|
+
onDrop,
|
|
287
|
+
className: cn(
|
|
288
|
+
"flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
|
|
289
|
+
dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
|
|
290
|
+
),
|
|
291
|
+
children: [
|
|
292
|
+
/* @__PURE__ */ jsx2(ImageUp, { className: "size-8 text-panel-muted-fg" }),
|
|
293
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
|
|
294
|
+
/* @__PURE__ */ jsx2("span", { children: labels.imagePickerDrop }),
|
|
295
|
+
/* @__PURE__ */ jsx2(
|
|
296
|
+
Button,
|
|
297
|
+
{
|
|
298
|
+
type: "button",
|
|
299
|
+
size: "sm",
|
|
300
|
+
className: "bg-brand text-brand-fg hover:bg-brand/90",
|
|
301
|
+
onClick: () => fileInput.current?.click(),
|
|
302
|
+
children: labels.imagePickerBrowse
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
] }),
|
|
306
|
+
/* @__PURE__ */ jsx2(
|
|
307
|
+
"input",
|
|
308
|
+
{
|
|
309
|
+
ref: fileInput,
|
|
310
|
+
type: "file",
|
|
311
|
+
accept: library.accept ?? "image/*",
|
|
312
|
+
multiple: true,
|
|
313
|
+
hidden: true,
|
|
314
|
+
onChange: (e) => {
|
|
315
|
+
void upload(e.target.files);
|
|
316
|
+
e.target.value = "";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
]
|
|
321
|
+
}
|
|
305
322
|
)),
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
children: /* @__PURE__ */ jsx2(
|
|
321
|
-
"img",
|
|
323
|
+
library?.allowUrl !== false && /* @__PURE__ */ jsxs(
|
|
324
|
+
"form",
|
|
325
|
+
{
|
|
326
|
+
className: "flex gap-2",
|
|
327
|
+
onSubmit: (e) => {
|
|
328
|
+
e.preventDefault();
|
|
329
|
+
const value = url.trim();
|
|
330
|
+
if (value) onSelect(value);
|
|
331
|
+
},
|
|
332
|
+
children: [
|
|
333
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
334
|
+
/* @__PURE__ */ jsx2(Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
|
|
335
|
+
/* @__PURE__ */ jsx2(
|
|
336
|
+
Input,
|
|
322
337
|
{
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
className: "
|
|
338
|
+
value: url,
|
|
339
|
+
onChange: (e) => setUrl(e.target.value),
|
|
340
|
+
placeholder: labels.imagePickerUrlPlaceholder,
|
|
341
|
+
className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
|
|
327
342
|
}
|
|
328
343
|
)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
344
|
+
] }),
|
|
345
|
+
/* @__PURE__ */ jsx2(
|
|
346
|
+
Button,
|
|
347
|
+
{
|
|
348
|
+
type: "submit",
|
|
349
|
+
size: "lg",
|
|
350
|
+
variant: "outline",
|
|
351
|
+
disabled: !url.trim(),
|
|
352
|
+
className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
|
|
353
|
+
children: labels.imagePickerUseUrl
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
),
|
|
359
|
+
library && /* @__PURE__ */ jsxs("section", { children: [
|
|
360
|
+
/* @__PURE__ */ jsx2("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
|
|
361
|
+
loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ jsx2("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ jsx2(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ jsx2("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ jsxs("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
|
|
362
|
+
Array.from({ length: pending }, (_, i) => /* @__PURE__ */ jsx2(
|
|
363
|
+
"li",
|
|
335
364
|
{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
365
|
+
className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
|
|
366
|
+
children: /* @__PURE__ */ jsx2(Spinner, {})
|
|
367
|
+
},
|
|
368
|
+
`pending-${i}`
|
|
369
|
+
)),
|
|
370
|
+
images.map((image) => {
|
|
371
|
+
const selected = image.url === currentUrl;
|
|
372
|
+
const confirming = confirmId === image.id;
|
|
373
|
+
return /* @__PURE__ */ jsxs("li", { className: "group relative", children: [
|
|
374
|
+
/* @__PURE__ */ jsx2(
|
|
375
|
+
"button",
|
|
376
|
+
{
|
|
377
|
+
type: "button",
|
|
378
|
+
onClick: () => onSelect(image.url),
|
|
379
|
+
title: image.name,
|
|
380
|
+
className: cn(
|
|
381
|
+
"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",
|
|
382
|
+
selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
|
|
383
|
+
),
|
|
384
|
+
children: /* @__PURE__ */ jsx2(
|
|
385
|
+
"img",
|
|
386
|
+
{
|
|
387
|
+
src: image.url,
|
|
388
|
+
alt: image.name ?? "",
|
|
389
|
+
loading: "lazy",
|
|
390
|
+
className: "size-full object-contain"
|
|
391
|
+
}
|
|
392
|
+
)
|
|
393
|
+
}
|
|
344
394
|
),
|
|
345
|
-
children:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
395
|
+
selected && /* @__PURE__ */ jsx2("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__ */ jsx2(Check, { className: "size-3" }) }),
|
|
396
|
+
image.name && /* @__PURE__ */ jsx2("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 }),
|
|
397
|
+
library.remove && /* @__PURE__ */ jsxs(
|
|
398
|
+
"button",
|
|
399
|
+
{
|
|
400
|
+
type: "button",
|
|
401
|
+
onClick: () => void remove(image),
|
|
402
|
+
onBlur: () => confirming && setConfirmId(null),
|
|
403
|
+
title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
|
|
404
|
+
"aria-label": labels.imagePickerDelete,
|
|
405
|
+
className: cn(
|
|
406
|
+
"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",
|
|
407
|
+
confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
|
|
408
|
+
),
|
|
409
|
+
children: [
|
|
410
|
+
/* @__PURE__ */ jsx2(Trash2, { className: "size-3.5" }),
|
|
411
|
+
confirming && labels.imagePickerConfirmDelete
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
] }, image.id);
|
|
416
|
+
})
|
|
417
|
+
] })
|
|
418
|
+
] })
|
|
353
419
|
] })
|
|
354
|
-
]
|
|
355
|
-
|
|
356
|
-
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
) });
|
|
357
423
|
}
|
|
358
424
|
function matchesAccept(file, accept) {
|
|
359
425
|
if (!accept) return file.type.startsWith("image/");
|
|
@@ -397,6 +463,7 @@ import {
|
|
|
397
463
|
} from "lucide-react";
|
|
398
464
|
|
|
399
465
|
// src/components/editor/inspector/values.ts
|
|
466
|
+
import { isSafeLink, isSafeNavbarLink } from "@mjml-agent-editor/core";
|
|
400
467
|
var SIDES = ["top", "right", "bottom", "left"];
|
|
401
468
|
function expandBox(value) {
|
|
402
469
|
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
@@ -427,15 +494,6 @@ function parseNetwork(name) {
|
|
|
427
494
|
function networkName(network, share) {
|
|
428
495
|
return share || network === "custom" ? network : `${network}${NO_SHARE}`;
|
|
429
496
|
}
|
|
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
497
|
|
|
440
498
|
// src/components/editor/inspector/controls.tsx
|
|
441
499
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -1241,6 +1299,23 @@ function withoutEditorProps(mjml) {
|
|
|
1241
1299
|
)
|
|
1242
1300
|
);
|
|
1243
1301
|
}
|
|
1302
|
+
function withClosedTags(mjml) {
|
|
1303
|
+
return mjml.replace(START_TAG, (tag) => {
|
|
1304
|
+
const selfClosed = /^<(mj-[\w-]+)([\s\S]*?)\s*\/>$/i.exec(tag);
|
|
1305
|
+
return selfClosed ? `<${selfClosed[1]}${selfClosed[2]}></${selfClosed[1]}>` : tag;
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
var TABLE = /<!--[\s\S]*?(?:-->|$)|(<mj-table\b(?:[^>"']|"[^"]*"|'[^']*')*>)((?:<!--[\s\S]*?-->|(?!<!--)[\s\S])*?)(<\/mj-table\s*>)/gi;
|
|
1309
|
+
function withTableRowsKept(mjml) {
|
|
1310
|
+
return mjml.replace(
|
|
1311
|
+
TABLE,
|
|
1312
|
+
(match, open, rows, close) => open ? `${open}<template>${rows}</template>${close}` : match
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
function sourceForEditor(mjml) {
|
|
1316
|
+
const cleaned = withoutEditorProps(mjml);
|
|
1317
|
+
return { mjml: withTableRowsKept(withClosedTags(cleaned)), repaired: cleaned !== mjml };
|
|
1318
|
+
}
|
|
1244
1319
|
function illegalAttributes(tag, attributes) {
|
|
1245
1320
|
const allowed = ALLOWED_ATTRIBUTES[tag];
|
|
1246
1321
|
if (!allowed) return [];
|
|
@@ -2117,7 +2192,10 @@ var ICONS = {
|
|
|
2117
2192
|
"mj-navbar-link": svg(
|
|
2118
2193
|
'<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
2194
|
),
|
|
2120
|
-
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>')
|
|
2195
|
+
"mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>'),
|
|
2196
|
+
"mj-table": svg(
|
|
2197
|
+
'<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/>'
|
|
2198
|
+
)
|
|
2121
2199
|
};
|
|
2122
2200
|
function tagOf(c) {
|
|
2123
2201
|
return String(c.get("tagName") || c.get("type") || "");
|
|
@@ -2893,8 +2971,15 @@ function Header({
|
|
|
2893
2971
|
] });
|
|
2894
2972
|
}
|
|
2895
2973
|
|
|
2974
|
+
// src/components/editor/blocks.ts
|
|
2975
|
+
import grapesjs from "grapesjs";
|
|
2976
|
+
import grapesjsMJML from "grapesjs-mjml";
|
|
2977
|
+
|
|
2896
2978
|
// src/components/editor/ids.ts
|
|
2897
|
-
var ID_RE = {
|
|
2979
|
+
var ID_RE = {
|
|
2980
|
+
sec: /(?:^|\s)sec-([A-Za-z0-9_-]+)(?=\s|$)/,
|
|
2981
|
+
obj: /(?:^|\s)obj-([A-Za-z0-9_-]+)(?=\s|$)/
|
|
2982
|
+
};
|
|
2898
2983
|
function idOf(c, prefix) {
|
|
2899
2984
|
return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
|
|
2900
2985
|
}
|
|
@@ -2923,19 +3008,292 @@ function freshenCopiedIds(root, added) {
|
|
|
2923
3008
|
const id = idOf(c, prefix);
|
|
2924
3009
|
if (!id || !used(prefix, id, c)) continue;
|
|
2925
3010
|
const cssClass = String(c.getAttributes()["css-class"]);
|
|
2926
|
-
|
|
3011
|
+
const fresh = cssClass.replace(
|
|
3012
|
+
ID_RE[prefix],
|
|
3013
|
+
(token) => token.replace(`${prefix}-${id}`, `${prefix}-${newId()}`)
|
|
3014
|
+
);
|
|
3015
|
+
c.addAttributes({ "css-class": fresh });
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// src/components/editor/insert-block.ts
|
|
3021
|
+
var addsBlock = (e) => (e.key === "Enter" || e.key === " ") && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
3022
|
+
var tileAttributes = (id) => ({ tabindex: "0", role: "button", "data-block": id });
|
|
3023
|
+
var tagOf5 = (c) => String(c.get("tagName") ?? "");
|
|
3024
|
+
function closest(c, tag) {
|
|
3025
|
+
for (let cur = c; cur; cur = cur.parent() ?? void 0) if (tagOf5(cur) === tag) return cur;
|
|
3026
|
+
return void 0;
|
|
3027
|
+
}
|
|
3028
|
+
function childOf(parent, c) {
|
|
3029
|
+
let cur = c;
|
|
3030
|
+
while (cur.parent() && cur.parent() !== parent) cur = cur.parent();
|
|
3031
|
+
return cur;
|
|
3032
|
+
}
|
|
3033
|
+
function insertBlock(editor, block) {
|
|
3034
|
+
const content = block.getContent();
|
|
3035
|
+
let body;
|
|
3036
|
+
editor.getWrapper()?.onAll((c) => {
|
|
3037
|
+
if (!body && tagOf5(c) === "mj-body") body = c;
|
|
3038
|
+
});
|
|
3039
|
+
if (typeof content !== "string" || !body) return void 0;
|
|
3040
|
+
const selected = editor.getSelected();
|
|
3041
|
+
const section = closest(selected, "mj-section");
|
|
3042
|
+
const sections = section?.parent() ?? body;
|
|
3043
|
+
const afterSection = section ? { at: section.index() + 1 } : {};
|
|
3044
|
+
const column = closest(selected, "mj-column");
|
|
3045
|
+
let added;
|
|
3046
|
+
if (/^\s*<mj-section\b/i.test(content)) {
|
|
3047
|
+
added = sections.append(content, afterSection)[0];
|
|
3048
|
+
} else if (column && selected) {
|
|
3049
|
+
const at = selected === column ? {} : { at: childOf(column, selected).index() + 1 };
|
|
3050
|
+
added = column.append(content, at)[0];
|
|
3051
|
+
} else {
|
|
3052
|
+
const [wrapper] = sections.append(
|
|
3053
|
+
`<mj-section><mj-column>${content}</mj-column></mj-section>`,
|
|
3054
|
+
afterSection
|
|
3055
|
+
);
|
|
3056
|
+
added = wrapper?.components().at(0)?.components().at(0);
|
|
3057
|
+
}
|
|
3058
|
+
if (!added) return void 0;
|
|
3059
|
+
editor.select(added);
|
|
3060
|
+
if (block.get("activate")) added.getView()?.onActive(void 0);
|
|
3061
|
+
return added;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
// src/components/editor/blocks.ts
|
|
3065
|
+
var EDITABLE_TAGS = /* @__PURE__ */ new Set([
|
|
3066
|
+
"mj-section",
|
|
3067
|
+
"mj-group",
|
|
3068
|
+
"mj-column",
|
|
3069
|
+
"mj-text",
|
|
3070
|
+
"mj-image",
|
|
3071
|
+
"mj-button",
|
|
3072
|
+
"mj-divider",
|
|
3073
|
+
"mj-spacer",
|
|
3074
|
+
"mj-social",
|
|
3075
|
+
"mj-social-element",
|
|
3076
|
+
"mj-navbar",
|
|
3077
|
+
"mj-navbar-link"
|
|
3078
|
+
]);
|
|
3079
|
+
var CONTENT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-social-element", "mj-navbar-link"]);
|
|
3080
|
+
var EDITOR_INTERNALS = /data-gjs-|__p\b|custom-name/i;
|
|
3081
|
+
var DROPPED_MARKUP = /<(?:script|style|link|meta|iframe|object|embed|form)\b/i;
|
|
3082
|
+
var INLINE_IMAGE = /\b(?:src|srcset|background-url|icon)\s*=\s*(?:"[^"]*data:|'[^']*data:|data:)/i;
|
|
3083
|
+
var LINK_IN_MARKUP = /[\s/"'](?:xlink:)?(?:href|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
3084
|
+
var validator;
|
|
3085
|
+
function blockProblems(mjml, editor = validator ??= grapesjs.init({
|
|
3086
|
+
headless: true,
|
|
3087
|
+
storageManager: false,
|
|
3088
|
+
telemetry: false,
|
|
3089
|
+
plugins: [grapesjsMJML]
|
|
3090
|
+
})) {
|
|
3091
|
+
if (EDITOR_INTERNALS.test(mjml)) return ["it contains GrapesJS's own attributes"];
|
|
3092
|
+
if (INLINE_IMAGE.test(mjml)) return ["it inlines an image as a data: URL"];
|
|
3093
|
+
let tree;
|
|
3094
|
+
try {
|
|
3095
|
+
tree = editor.runCommand("mjml-code-to-html", {
|
|
3096
|
+
mjml: `<mjml><mj-body>${withClosedTags(mjml)}</mj-body></mjml>`,
|
|
3097
|
+
validationLevel: "strict"
|
|
3098
|
+
}).json;
|
|
3099
|
+
} catch (e) {
|
|
3100
|
+
const errors = e.errors;
|
|
3101
|
+
if (!errors) throw e;
|
|
3102
|
+
return errors.map((error) => error.formattedMessage ?? error.message ?? String(error));
|
|
3103
|
+
}
|
|
3104
|
+
const problems = [];
|
|
3105
|
+
const top = tree.children?.find((c) => c.tagName === "mj-body")?.children ?? [];
|
|
3106
|
+
if (top.length !== 1 || top[0].tagName !== "mj-section") {
|
|
3107
|
+
problems.push("it is not exactly one mj-section");
|
|
3108
|
+
}
|
|
3109
|
+
const visit = (node, parent) => {
|
|
3110
|
+
const tag = node.tagName;
|
|
3111
|
+
const attributes = node.attributes ?? {};
|
|
3112
|
+
const content = node.content ?? "";
|
|
3113
|
+
if (!EDITABLE_TAGS.has(tag)) problems.push(`${tag} is not editable here`);
|
|
3114
|
+
const cssClass = attributes["css-class"] ?? "";
|
|
3115
|
+
if (ID_RE.sec.test(cssClass) || ID_RE.obj.test(cssClass)) {
|
|
3116
|
+
problems.push(`${tag} carries a sec-* or obj-* id`);
|
|
3117
|
+
}
|
|
3118
|
+
if ("mj-class" in attributes) problems.push(`${tag} uses mj-class`);
|
|
3119
|
+
const links = [attributes.href ?? ""];
|
|
3120
|
+
for (const match of content.matchAll(LINK_IN_MARKUP)) {
|
|
3121
|
+
links.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
3122
|
+
}
|
|
3123
|
+
const linked = tag === "mj-navbar-link" ? isSafeNavbarLink(parent?.attributes?.["base-url"] ?? "", attributes.href ?? "") : true;
|
|
3124
|
+
if (!linked || !links.every(isSafeLink)) {
|
|
3125
|
+
problems.push(`${tag} has a link scheme mail must not carry`);
|
|
3126
|
+
}
|
|
3127
|
+
if (CONTENT_TAGS.has(tag) ? /<mj-/i.test(content) || DROPPED_MARKUP.test(content) : content.trim()) {
|
|
3128
|
+
problems.push(`${tag} holds content the editor does not keep`);
|
|
3129
|
+
}
|
|
3130
|
+
node.children?.forEach((child) => visit(child, node));
|
|
3131
|
+
};
|
|
3132
|
+
top.forEach((node) => visit(node));
|
|
3133
|
+
return problems;
|
|
3134
|
+
}
|
|
3135
|
+
var escapeHtml3 = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3136
|
+
function registerBlocks(editor, blocks, category, media) {
|
|
3137
|
+
for (const block of blocks) {
|
|
3138
|
+
const problems = editor.Blocks.get(block.id) ? [`the id is already taken`] : blockProblems(block.mjml, editor);
|
|
3139
|
+
if (problems.length > 0) {
|
|
3140
|
+
console.error(`Block "${block.id}" is left out of the palette: ${problems.join("; ")}`);
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
editor.Blocks.add(block.id, {
|
|
3144
|
+
// GrapesJS writes the label into the tile as markup.
|
|
3145
|
+
label: escapeHtml3(block.label),
|
|
3146
|
+
category,
|
|
3147
|
+
media: block.media ?? media,
|
|
3148
|
+
content: withClosedTags(block.mjml),
|
|
3149
|
+
attributes: tileAttributes(block.id)
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
// src/components/editor/builtin-blocks.ts
|
|
3155
|
+
var BUILTIN_BLOCKS = [
|
|
3156
|
+
"mj-1-column",
|
|
3157
|
+
"mj-2-columns",
|
|
3158
|
+
"mj-3-columns",
|
|
3159
|
+
"mj-text",
|
|
3160
|
+
"mj-image",
|
|
3161
|
+
"mj-button",
|
|
3162
|
+
"mj-divider",
|
|
3163
|
+
"mj-spacer",
|
|
3164
|
+
"mj-social-group",
|
|
3165
|
+
"mj-navbar"
|
|
3166
|
+
];
|
|
3167
|
+
var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
|
|
3168
|
+
var CONTENT2 = {
|
|
3169
|
+
// The plugin's version links Google+ and shares the page: a bare network name makes the
|
|
3170
|
+
// icon open a "share this link" dialog, while `-noshare` links to the profile a footer
|
|
3171
|
+
// means. Without `alt` each link is an unnamed image to a screen reader.
|
|
3172
|
+
"mj-social-group": `<mj-social icon-size="24px" mode="horizontal">
|
|
3173
|
+
<mj-social-element name="facebook-noshare" href="https://facebook.com/" alt="Facebook"></mj-social-element>
|
|
3174
|
+
<mj-social-element name="instagram-noshare" href="https://instagram.com/" alt="Instagram"></mj-social-element>
|
|
3175
|
+
<mj-social-element name="x-noshare" href="https://x.com/" alt="X"></mj-social-element>
|
|
3176
|
+
</mj-social>`,
|
|
3177
|
+
// The plugin's sample is a via.placeholder.com picture, stored in the email and loaded by
|
|
3178
|
+
// every reader's mail client. Dropping one opens the image picker instead.
|
|
3179
|
+
"mj-image": "<mj-image></mj-image>"
|
|
3180
|
+
};
|
|
3181
|
+
function builtinBlock(labels) {
|
|
3182
|
+
return (id) => ({
|
|
3183
|
+
label: labels.blockLabels[id] ?? id,
|
|
3184
|
+
// By id: a regex on the label broke as soon as a host translated it.
|
|
3185
|
+
category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
|
|
3186
|
+
...id in CONTENT2 && { content: CONTENT2[id] },
|
|
3187
|
+
attributes: tileAttributes(id)
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
var CANVAS_CSS = 'img:not([src]), img[src=""] { min-height: 120px; background: #f4f4f5; outline: 1px dashed #a1a1aa; outline-offset: -1px; }';
|
|
3191
|
+
|
|
3192
|
+
// src/components/editor/table-type.ts
|
|
3193
|
+
import { isSafeLink as isSafeLink2 } from "@mjml-agent-editor/core";
|
|
3194
|
+
var DROPPED = "script, style, link, iframe, frame, object, embed, applet, base, meta, animate, set, svg, math, template";
|
|
3195
|
+
var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href", "action", "formaction"]);
|
|
3196
|
+
function cleanRows(rows) {
|
|
3197
|
+
rows.querySelectorAll(DROPPED).forEach((el) => el.remove());
|
|
3198
|
+
rows.querySelectorAll("*").forEach((el) => {
|
|
3199
|
+
for (const { name, value } of [...el.attributes]) {
|
|
3200
|
+
const lower = name.toLowerCase();
|
|
3201
|
+
const script = /^(java|vb)script:/i.test(value.replace(/[\u0000-\u0020]/g, ""));
|
|
3202
|
+
if (lower.startsWith("on") || script || LINK_ATTRIBUTES.has(lower) && !isSafeLink2(value)) {
|
|
3203
|
+
el.removeAttribute(name);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
var ROW_STYLE = "pointer-events: all; display: table; width: 100%;";
|
|
3209
|
+
function tablePreview(doc, attributes, rows) {
|
|
3210
|
+
const attr = (name, fallback) => String(attributes[name] ?? fallback);
|
|
3211
|
+
const template = doc.createElement("template");
|
|
3212
|
+
template.innerHTML = `<table>${rows}</table>`;
|
|
3213
|
+
const table = doc.createElement("table");
|
|
3214
|
+
table.setAttribute("cellpadding", String(parseInt(attr("cellpadding", "0"), 10) || 0));
|
|
3215
|
+
table.setAttribute("cellspacing", String(parseInt(attr("cellspacing", "0"), 10) || 0));
|
|
3216
|
+
Object.assign(table.style, {
|
|
3217
|
+
width: attr("width", "100%"),
|
|
3218
|
+
border: attr("border", "none"),
|
|
3219
|
+
color: attr("color", "#000000"),
|
|
3220
|
+
fontFamily: attr("font-family", "Ubuntu, Helvetica, Arial, sans-serif"),
|
|
3221
|
+
fontSize: attr("font-size", "13px"),
|
|
3222
|
+
lineHeight: attr("line-height", "22px"),
|
|
3223
|
+
tableLayout: attr("table-layout", "auto")
|
|
3224
|
+
});
|
|
3225
|
+
for (const row of template.content.querySelectorAll("tr")) {
|
|
3226
|
+
const tr = table.insertRow();
|
|
3227
|
+
for (const cell2 of row.querySelectorAll(":scope > th, :scope > td")) {
|
|
3228
|
+
const copy = doc.createElement(cell2.tagName.toLowerCase());
|
|
3229
|
+
copy.textContent = cell2.textContent;
|
|
3230
|
+
const align = cell2.getAttribute("align");
|
|
3231
|
+
if (align && /^(left|right|center)$/i.test(align)) copy.setAttribute("align", align);
|
|
3232
|
+
tr.append(copy);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
const cell = doc.createElement("td");
|
|
3236
|
+
cell.style.padding = attr("padding", "10px 25px");
|
|
3237
|
+
cell.append(table);
|
|
3238
|
+
return cell;
|
|
3239
|
+
}
|
|
3240
|
+
function addTableType(editor) {
|
|
3241
|
+
const base = editor.Components.getType("default").view.prototype;
|
|
3242
|
+
editor.Components.addType("mj-table", {
|
|
3243
|
+
isComponent: (el) => {
|
|
3244
|
+
if (el.tagName?.toLowerCase() !== "mj-table") return false;
|
|
3245
|
+
const rows = el.querySelector(":scope > template");
|
|
3246
|
+
if (rows) cleanRows(rows.content);
|
|
3247
|
+
return { type: "mj-table", content: rows?.innerHTML ?? "", components: [] };
|
|
3248
|
+
},
|
|
3249
|
+
model: { defaults: { tagName: "mj-table", droppable: false, editable: false } },
|
|
3250
|
+
view: {
|
|
3251
|
+
tagName: "tr",
|
|
3252
|
+
// GrapesJS clears the element's attributes, style among them, when the model's change.
|
|
3253
|
+
updateAttributes(...args) {
|
|
3254
|
+
base.updateAttributes.apply(this, args);
|
|
3255
|
+
this.el.style.cssText = ROW_STYLE;
|
|
3256
|
+
},
|
|
3257
|
+
updateContent() {
|
|
3258
|
+
const { el, model } = this;
|
|
3259
|
+
el.style.cssText = ROW_STYLE;
|
|
3260
|
+
el.replaceChildren(
|
|
3261
|
+
tablePreview(el.ownerDocument, model.getAttributes(), String(model.get("content") ?? ""))
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
2927
3264
|
}
|
|
2928
3265
|
});
|
|
2929
3266
|
}
|
|
2930
3267
|
|
|
2931
3268
|
// src/components/editor/plugin-types.ts
|
|
3269
|
+
var COLUMN_CHILDREN = /* @__PURE__ */ new Set([
|
|
3270
|
+
"mj-accordion",
|
|
3271
|
+
"mj-button",
|
|
3272
|
+
"mj-carousel",
|
|
3273
|
+
"mj-divider",
|
|
3274
|
+
"mj-image",
|
|
3275
|
+
"mj-navbar",
|
|
3276
|
+
"mj-raw",
|
|
3277
|
+
"mj-social",
|
|
3278
|
+
"mj-spacer",
|
|
3279
|
+
"mj-table",
|
|
3280
|
+
"mj-text"
|
|
3281
|
+
]);
|
|
2932
3282
|
function fixPluginTypes(editor) {
|
|
3283
|
+
addTableType(editor);
|
|
2933
3284
|
for (const type of editor.Components.getTypes()) {
|
|
2934
|
-
if (!type.id.startsWith("mj-")) continue;
|
|
3285
|
+
if (!type.id.startsWith("mj-") || type.id === "mj-table") continue;
|
|
2935
3286
|
const base = type.model.prototype.getAttrToHTML;
|
|
2936
3287
|
editor.Components.addType(type.id, {
|
|
2937
3288
|
model: {
|
|
2938
|
-
defaults: {
|
|
3289
|
+
defaults: {
|
|
3290
|
+
"style-default": {},
|
|
3291
|
+
...type.id === "mj-column" && {
|
|
3292
|
+
droppable: (source) => COLUMN_CHILDREN.has(String(source.get("tagName")))
|
|
3293
|
+
},
|
|
3294
|
+
// GrapesJS's own is an SVG's markup, which the canvas wrote into `src` and saved.
|
|
3295
|
+
...type.id === "mj-image" && { src: "" }
|
|
3296
|
+
},
|
|
2939
3297
|
getAttrToHTML() {
|
|
2940
3298
|
const attributes = base.call(this);
|
|
2941
3299
|
delete attributes.__p;
|
|
@@ -2957,16 +3315,17 @@ var TYPE_LABEL = {
|
|
|
2957
3315
|
"mj-image": "Image",
|
|
2958
3316
|
"mj-column": "Column"
|
|
2959
3317
|
};
|
|
2960
|
-
function
|
|
3318
|
+
function tagOf6(c) {
|
|
2961
3319
|
return String(c.get("tagName") || c.get("type") || "");
|
|
2962
3320
|
}
|
|
2963
3321
|
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
3322
|
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>';
|
|
3323
|
+
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
3324
|
function isSection(c) {
|
|
2966
|
-
return
|
|
3325
|
+
return tagOf6(c) === "mj-section";
|
|
2967
3326
|
}
|
|
2968
3327
|
function isCommentable(c) {
|
|
2969
|
-
const t =
|
|
3328
|
+
const t = tagOf6(c);
|
|
2970
3329
|
return t === "mj-section" || t in TYPE_LABEL;
|
|
2971
3330
|
}
|
|
2972
3331
|
function closestSection(c) {
|
|
@@ -2978,29 +3337,11 @@ function closestSection(c) {
|
|
|
2978
3337
|
return null;
|
|
2979
3338
|
}
|
|
2980
3339
|
function crumbName(c, labels) {
|
|
2981
|
-
const t =
|
|
3340
|
+
const t = tagOf6(c);
|
|
2982
3341
|
return labels.elementNames[t] ?? t.replace(/^mj-/, "");
|
|
2983
3342
|
}
|
|
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
3343
|
function objectLabel(c) {
|
|
3003
|
-
const base = TYPE_LABEL[
|
|
3344
|
+
const base = TYPE_LABEL[tagOf6(c)] ?? tagOf6(c);
|
|
3004
3345
|
const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
|
|
3005
3346
|
return text ? `${base}: \u201C${text}\u201D` : base;
|
|
3006
3347
|
}
|
|
@@ -3068,7 +3409,13 @@ function setupRichText(editor, labels) {
|
|
|
3068
3409
|
result: (r, action) => r.exec("hiliteColor", val(action, "input"))
|
|
3069
3410
|
});
|
|
3070
3411
|
}
|
|
3071
|
-
function EmailEditor({
|
|
3412
|
+
function EmailEditor({
|
|
3413
|
+
docId,
|
|
3414
|
+
onReady,
|
|
3415
|
+
commentsRefresh,
|
|
3416
|
+
onOpenCountChange,
|
|
3417
|
+
blocks = []
|
|
3418
|
+
}) {
|
|
3072
3419
|
const documents = useDocumentStore();
|
|
3073
3420
|
const labels = useLabels();
|
|
3074
3421
|
const loadingRef = useRef5(false);
|
|
@@ -3090,8 +3437,15 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3090
3437
|
const readOnlyRef = useRef5(false);
|
|
3091
3438
|
const applyReadOnly = useRef5(async () => {
|
|
3092
3439
|
});
|
|
3093
|
-
|
|
3094
|
-
useEffect5(() => ()
|
|
3440
|
+
const [turn] = useState5(createTurnFollower);
|
|
3441
|
+
useEffect5(() => turn.turn(turnRunning), [turn, turnRunning]);
|
|
3442
|
+
useEffect5(() => {
|
|
3443
|
+
turn.mount();
|
|
3444
|
+
return () => {
|
|
3445
|
+
turn.unmount();
|
|
3446
|
+
setLocked(false);
|
|
3447
|
+
};
|
|
3448
|
+
}, [turn, setLocked]);
|
|
3095
3449
|
const [crumbs, setCrumbs] = useState5([]);
|
|
3096
3450
|
const [picker, setPicker] = useState5(
|
|
3097
3451
|
null
|
|
@@ -3102,7 +3456,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3102
3456
|
const chain = [];
|
|
3103
3457
|
let cur = c;
|
|
3104
3458
|
while (cur) {
|
|
3105
|
-
const t =
|
|
3459
|
+
const t = tagOf6(cur);
|
|
3106
3460
|
if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
|
|
3107
3461
|
chain.unshift(cur);
|
|
3108
3462
|
cur = cur.parent() ?? void 0;
|
|
@@ -3151,7 +3505,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3151
3505
|
if (isSection(c)) addIdClass(c, "sec");
|
|
3152
3506
|
if (!isCommentable(c)) return;
|
|
3153
3507
|
const toolbar = [...c.get("toolbar") ?? []];
|
|
3154
|
-
if (
|
|
3508
|
+
if (tagOf6(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
|
|
3155
3509
|
toolbar.push({
|
|
3156
3510
|
command: "open-image-picker",
|
|
3157
3511
|
label: IMAGE_SVG,
|
|
@@ -3171,7 +3525,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3171
3525
|
editor.Commands.add("open-image-picker", {
|
|
3172
3526
|
run(ed) {
|
|
3173
3527
|
const target = ed.getSelected();
|
|
3174
|
-
if (!target ||
|
|
3528
|
+
if (!target || tagOf6(target) !== "mj-image") return;
|
|
3175
3529
|
const am = ed.AssetManager;
|
|
3176
3530
|
am.open({
|
|
3177
3531
|
target,
|
|
@@ -3191,6 +3545,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3191
3545
|
});
|
|
3192
3546
|
setupRichText(editor, labels);
|
|
3193
3547
|
fixPluginTypes(editor);
|
|
3548
|
+
registerBlocks(editor, blocks, labels.blockCategorySections, SECTION_SVG);
|
|
3194
3549
|
setupLayerNames(editor, labels);
|
|
3195
3550
|
const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
|
|
3196
3551
|
const showBodyInLayers = () => {
|
|
@@ -3198,14 +3553,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3198
3553
|
if (!body) return;
|
|
3199
3554
|
editor.Layers.setRoot(body);
|
|
3200
3555
|
body.onAll((c) => {
|
|
3201
|
-
if (STRUCTURE.has(
|
|
3556
|
+
if (STRUCTURE.has(tagOf6(c))) editor.Layers.setOpen(c, true);
|
|
3202
3557
|
});
|
|
3203
3558
|
};
|
|
3204
3559
|
const keepStructureOpen = () => {
|
|
3205
3560
|
const layers = editor.Layers;
|
|
3206
3561
|
const opened = layers.model?.get("opened") ?? {};
|
|
3207
3562
|
for (const [cid, c] of Object.entries(opened)) {
|
|
3208
|
-
if (STRUCTURE.has(
|
|
3563
|
+
if (STRUCTURE.has(tagOf6(c))) delete opened[cid];
|
|
3209
3564
|
}
|
|
3210
3565
|
};
|
|
3211
3566
|
editor.on("component:add", (c) => {
|
|
@@ -3233,16 +3588,16 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3233
3588
|
editor.on("component:deselected", () => {
|
|
3234
3589
|
buildCrumbs(editor.getSelected());
|
|
3235
3590
|
});
|
|
3236
|
-
|
|
3591
|
+
const baseline = createSaveBaseline();
|
|
3237
3592
|
const save = async () => {
|
|
3238
3593
|
const view = editor.getEditing()?.getView();
|
|
3239
3594
|
await view?.syncContent?.();
|
|
3240
3595
|
const mjml = editor.getHtml();
|
|
3241
|
-
if (mjml
|
|
3596
|
+
if (!baseline.changed(mjml)) return;
|
|
3242
3597
|
setSave("saving");
|
|
3243
3598
|
try {
|
|
3244
3599
|
await documents.save(docId, { mjml, projectData: editor.getProjectData() });
|
|
3245
|
-
|
|
3600
|
+
baseline.saved(mjml);
|
|
3246
3601
|
setSave("saved");
|
|
3247
3602
|
} catch (e) {
|
|
3248
3603
|
setSave("error");
|
|
@@ -3273,6 +3628,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3273
3628
|
});
|
|
3274
3629
|
}
|
|
3275
3630
|
const lock = (on) => {
|
|
3631
|
+
if (!turn.mounted) return;
|
|
3276
3632
|
readOnlyRef.current = on;
|
|
3277
3633
|
setLocked(on);
|
|
3278
3634
|
notifyState();
|
|
@@ -3298,37 +3654,36 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3298
3654
|
return;
|
|
3299
3655
|
}
|
|
3300
3656
|
lock(false);
|
|
3657
|
+
queue.schedule();
|
|
3301
3658
|
};
|
|
3302
3659
|
const reload = () => queue.run(async () => {
|
|
3303
3660
|
const fresh = await documents.get(docId);
|
|
3304
|
-
if (fresh.mjml !== stored) loadMjml(fresh.mjml);
|
|
3661
|
+
if (fresh.mjml !== baseline.stored) loadMjml(fresh.mjml);
|
|
3305
3662
|
});
|
|
3306
3663
|
const loadMjml = (mjml) => {
|
|
3307
3664
|
loadingRef.current = true;
|
|
3308
3665
|
let repaired = false;
|
|
3666
|
+
const adds = !mjml || listSections(mjml).some((s) => !s.sectionId);
|
|
3309
3667
|
try {
|
|
3310
3668
|
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
|
-
}
|
|
3669
|
+
baseline.load(mjml, null);
|
|
3670
|
+
const source = sourceForEditor(mjml || STARTER_MJML);
|
|
3671
|
+
repaired = source.repaired;
|
|
3672
|
+
editor.setComponents(source.mjml);
|
|
3319
3673
|
editor.getWrapper()?.onAll((c) => {
|
|
3320
|
-
const illegal = illegalAttributes(
|
|
3674
|
+
const illegal = illegalAttributes(tagOf6(c), c.getAttributes());
|
|
3321
3675
|
if (illegal.length === 0) return;
|
|
3322
3676
|
c.removeAttributes(illegal);
|
|
3323
3677
|
repaired = true;
|
|
3324
3678
|
});
|
|
3679
|
+
if (!repaired && !adds) baseline.load(mjml, editor.getHtml());
|
|
3325
3680
|
showBodyInLayers();
|
|
3326
3681
|
editor.UndoManager.clear();
|
|
3327
3682
|
} finally {
|
|
3328
3683
|
loadingRef.current = false;
|
|
3329
3684
|
}
|
|
3330
3685
|
notifyState();
|
|
3331
|
-
if (repaired && !readOnlyRef.current) queue.schedule();
|
|
3686
|
+
if ((repaired || adds) && !readOnlyRef.current) queue.schedule();
|
|
3332
3687
|
};
|
|
3333
3688
|
const ensureHighlightStyles = () => {
|
|
3334
3689
|
const cdoc = editor.Canvas.getDocument();
|
|
@@ -3350,7 +3705,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3350
3705
|
cdoc.head.appendChild(style);
|
|
3351
3706
|
};
|
|
3352
3707
|
const sectionEl = (sectionId) => {
|
|
3353
|
-
|
|
3708
|
+
let comp;
|
|
3709
|
+
editor.getWrapper()?.onAll((c) => {
|
|
3710
|
+
if (!comp && isSection(c) && sectionIdOf(c) === sectionId) comp = c;
|
|
3711
|
+
});
|
|
3354
3712
|
return comp?.getEl?.() ?? void 0;
|
|
3355
3713
|
};
|
|
3356
3714
|
const compiledHtml = () => {
|
|
@@ -3361,9 +3719,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3361
3719
|
return editor.getHtml();
|
|
3362
3720
|
}
|
|
3363
3721
|
};
|
|
3722
|
+
turn.ready((on) => void applyReadOnly.current(on));
|
|
3364
3723
|
try {
|
|
3365
|
-
|
|
3366
|
-
loadMjml(doc.mjml);
|
|
3724
|
+
await queue.run(async () => loadMjml((await documents.get(docId)).mjml));
|
|
3367
3725
|
} catch (e) {
|
|
3368
3726
|
toast2.error(labels.documentLoadFailed);
|
|
3369
3727
|
console.error(e);
|
|
@@ -3418,7 +3776,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3418
3776
|
return /* @__PURE__ */ jsx8(
|
|
3419
3777
|
GjsEditor,
|
|
3420
3778
|
{
|
|
3421
|
-
grapesjs,
|
|
3779
|
+
grapesjs: grapesjs2,
|
|
3422
3780
|
className: "h-full",
|
|
3423
3781
|
options: {
|
|
3424
3782
|
height: "100%",
|
|
@@ -3427,6 +3785,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3427
3785
|
// app.grapesjs.com and leave a session-storage entry, which a host would then have
|
|
3428
3786
|
// to disclose and, for the entry, ask consent for.
|
|
3429
3787
|
telemetry: false,
|
|
3788
|
+
canvasCss: CANVAS_CSS,
|
|
3430
3789
|
fromElement: false,
|
|
3431
3790
|
panels: { defaults: [] },
|
|
3432
3791
|
blockManager: { appendTo: "#gjs-blocks" },
|
|
@@ -3446,7 +3805,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3446
3805
|
close: () => setPicker(null)
|
|
3447
3806
|
}
|
|
3448
3807
|
},
|
|
3449
|
-
canvas: {
|
|
3808
|
+
canvas: {
|
|
3809
|
+
customBadgeLabel: badgeLabel(labels),
|
|
3810
|
+
// A file or text dragged in from outside lands as `<img>` or `<div>`, which no MJML
|
|
3811
|
+
// validator accepts; drags from the palette are unaffected.
|
|
3812
|
+
allowExternalDrop: false
|
|
3813
|
+
},
|
|
3450
3814
|
deviceManager: {
|
|
3451
3815
|
devices: [
|
|
3452
3816
|
{ id: "desktop", name: "Desktop", width: "" },
|
|
@@ -3454,14 +3818,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
3454
3818
|
]
|
|
3455
3819
|
},
|
|
3456
3820
|
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
|
-
})
|
|
3821
|
+
(editor) => grapesjsMJML2(editor, {
|
|
3822
|
+
blocks: BUILTIN_BLOCKS,
|
|
3823
|
+
block: builtinBlock(labels)
|
|
3465
3824
|
})
|
|
3466
3825
|
]
|
|
3467
3826
|
},
|
|
@@ -3543,7 +3902,7 @@ function LeftSidebar({
|
|
|
3543
3902
|
const selectBody = () => {
|
|
3544
3903
|
let body;
|
|
3545
3904
|
editor?.getWrapper()?.onAll((c) => {
|
|
3546
|
-
if (!body &&
|
|
3905
|
+
if (!body && tagOf6(c) === "mj-body") body = c;
|
|
3547
3906
|
});
|
|
3548
3907
|
if (body) editor?.select(body);
|
|
3549
3908
|
};
|
|
@@ -3551,6 +3910,13 @@ function LeftSidebar({
|
|
|
3551
3910
|
setLayersExpanded(expand);
|
|
3552
3911
|
editor?.getWrapper()?.onAll((c) => c.set("open", expand));
|
|
3553
3912
|
};
|
|
3913
|
+
const onTileKey = (e) => {
|
|
3914
|
+
const tile = e.target.closest("[data-block]");
|
|
3915
|
+
const block = tile && editor?.Blocks.get(tile.dataset.block ?? "");
|
|
3916
|
+
if (!editor || !block || !addsBlock(e)) return;
|
|
3917
|
+
e.preventDefault();
|
|
3918
|
+
if (insertBlock(editor, block)) toast2(labels.blockAdded(tile.textContent?.trim() ?? ""));
|
|
3919
|
+
};
|
|
3554
3920
|
const filterBlocks = (q) => {
|
|
3555
3921
|
setBlockSearch(q);
|
|
3556
3922
|
const root = document.getElementById("gjs-blocks");
|
|
@@ -3607,7 +3973,7 @@ function LeftSidebar({
|
|
|
3607
3973
|
/* @__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
3974
|
] }),
|
|
3609
3975
|
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" })
|
|
3976
|
+
/* @__PURE__ */ jsx8("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: onTileKey })
|
|
3611
3977
|
] }),
|
|
3612
3978
|
/* @__PURE__ */ jsx8("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: /* @__PURE__ */ jsx8(
|
|
3613
3979
|
Inspector,
|
|
@@ -3687,6 +4053,7 @@ function Breadcrumb({
|
|
|
3687
4053
|
] }, i)) });
|
|
3688
4054
|
}
|
|
3689
4055
|
export {
|
|
3690
|
-
EmailEditor
|
|
4056
|
+
EmailEditor,
|
|
4057
|
+
blockProblems
|
|
3691
4058
|
};
|
|
3692
4059
|
//# sourceMappingURL=canvas.js.map
|