react-email-studio 3.8.1 → 3.8.3
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/CHANGELOG.md +17 -0
- package/README.md +5 -2
- package/RELEASE.md +137 -0
- package/dist/index.cjs +143 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +143 -52
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.8.3] - 2026-05-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Context menu Move Up / Move Down** now works for all block types: root `blocks[]` sections (row reorder), stacked blocks in a `rows[]` column (in-column swap), nested layout columns, and single-block layout rows.
|
|
13
|
+
- **Context menu clicks** no longer dismiss the menu before the action runs (`pointerdown` handling on the menu overlay).
|
|
14
|
+
|
|
15
|
+
## [3.8.2] - 2026-05-22
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added **`RELEASE.md`** as a single-file release tutorial and included it in the published npm package.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Text block newline handling** is now consistent in canvas preview and exported HTML: plain text line breaks render as `<br/>`, while HTML content remains passthrough.
|
|
24
|
+
|
|
8
25
|
## [3.8.0] - 2026-05-15
|
|
9
26
|
|
|
10
27
|
### Changed
|
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
## Release notes
|
|
9
9
|
|
|
10
|
-
**Latest: 3.8.
|
|
10
|
+
**Latest: 3.8.3** — fixes context menu **Move Up / Move Down** for root sections, stacked column blocks, and nested layout blocks.
|
|
11
|
+
|
|
12
|
+
See **3.8.2** for the **`RELEASE.md`** npm tutorial and plain-text newline rendering fixes.
|
|
11
13
|
|
|
12
14
|
Version history and migration hints: **[CHANGELOG.md](./CHANGELOG.md)** (also included in the published npm tarball under `node_modules/react-email-studio/CHANGELOG.md`).
|
|
13
15
|
|
|
@@ -121,7 +123,8 @@ Implement **`onUpload`** so image/video uploads return URLs your recipients can
|
|
|
121
123
|
| `ReactEmailEditorOptions`, `JsonToHtmlOptions`, `EmailHtmlOptions` | Editor options and HTML generation options. |
|
|
122
124
|
| `EmailDocument`, `EmailDocumentSettings`, … | JSON schema types for stored designs. |
|
|
123
125
|
|
|
124
|
-
For
|
|
126
|
+
For a single-file release tutorial, see **[RELEASE.md](./RELEASE.md)**.
|
|
127
|
+
For extended framework setup (Next.js client/SSR), props tables, and troubleshooting, see **[TUTORIAL.md](./TUTORIAL.md)**.
|
|
125
128
|
|
|
126
129
|
---
|
|
127
130
|
|
package/RELEASE.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# react-email-studio release tutorial (single file)
|
|
2
|
+
|
|
3
|
+
Use this file as the one-stop guide for npm integration, save/load, HTML export, and API usage.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install react-email-studio
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Install peer dependencies in your app:
|
|
12
|
+
|
|
13
|
+
- `react`, `react-dom`
|
|
14
|
+
- `lucide-react`
|
|
15
|
+
- TipTap v3:
|
|
16
|
+
- `@tiptap/react`
|
|
17
|
+
- `@tiptap/core`
|
|
18
|
+
- `@tiptap/starter-kit`
|
|
19
|
+
- `@tiptap/extension-link`
|
|
20
|
+
- `@tiptap/extension-placeholder`
|
|
21
|
+
- `@tiptap/extension-text-align`
|
|
22
|
+
- `@tiptap/extension-text-style`
|
|
23
|
+
- `@tiptap/extension-underline`
|
|
24
|
+
|
|
25
|
+
## Minimal integration
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { useRef, useCallback } from "react";
|
|
29
|
+
import {
|
|
30
|
+
ReactEmailEditor,
|
|
31
|
+
type ReactEmailEditorRef,
|
|
32
|
+
jsonToHtml,
|
|
33
|
+
} from "react-email-studio";
|
|
34
|
+
|
|
35
|
+
export function MailStudioPage() {
|
|
36
|
+
const ref = useRef<ReactEmailEditorRef>(null);
|
|
37
|
+
|
|
38
|
+
const save = useCallback(() => {
|
|
39
|
+
ref.current?.exportJson((jsonString) => {
|
|
40
|
+
void fetch("/api/email-designs", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/json" },
|
|
43
|
+
body: JSON.stringify({ design: jsonString }),
|
|
44
|
+
});
|
|
45
|
+
}, true);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<button type="button" onClick={save}>Save design</button>
|
|
51
|
+
<ReactEmailEditor
|
|
52
|
+
ref={ref}
|
|
53
|
+
hideTemplates
|
|
54
|
+
onUpload={uploadImageAndReturnUrl}
|
|
55
|
+
onReady={(api) => {
|
|
56
|
+
// api.loadJson(savedDesignJsonOrObject);
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function uploadImageAndReturnUrl(file: File): Promise<string> {
|
|
64
|
+
const body = new FormData();
|
|
65
|
+
body.append("file", file);
|
|
66
|
+
const res = await fetch("/api/upload", { method: "POST", body });
|
|
67
|
+
const { url } = await res.json();
|
|
68
|
+
return url as string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function htmlFromStoredDesign(designJson: string): string {
|
|
72
|
+
return jsonToHtml(designJson);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Important: `onUpload` must return public HTTPS URLs that recipients can access.
|
|
77
|
+
|
|
78
|
+
## Next.js note
|
|
79
|
+
|
|
80
|
+
The editor is client-side. Use:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
"use client";
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If needed:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import dynamic from "next/dynamic";
|
|
90
|
+
const ReactEmailEditor = dynamic(
|
|
91
|
+
() => import("react-email-studio").then((m) => m.ReactEmailEditor),
|
|
92
|
+
{ ssr: false },
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Core API
|
|
97
|
+
|
|
98
|
+
- `ref.loadJson(input)`:
|
|
99
|
+
- Accepts JSON string or object.
|
|
100
|
+
- Replaces editor content.
|
|
101
|
+
- `ref.exportJson(cb, pretty?)`:
|
|
102
|
+
- Returns canonical `email_document` JSON.
|
|
103
|
+
- `jsonToHtml(design, opts?)`:
|
|
104
|
+
- Converts design JSON to full HTML document.
|
|
105
|
+
- `htmlToJson(html, pretty?)`:
|
|
106
|
+
- Converts pasted HTML to `email_document` JSON.
|
|
107
|
+
|
|
108
|
+
## Exported helpers
|
|
109
|
+
|
|
110
|
+
- `htmlToEmailDesignTemplate`
|
|
111
|
+
- `extractHtmlForDesign`
|
|
112
|
+
- `canonicalizeEmailDocument`
|
|
113
|
+
- `utf8ToBase64`, `base64ToUtf8`
|
|
114
|
+
- `EmailPreviewModal`, `emailPreviewDevices`
|
|
115
|
+
|
|
116
|
+
## JSON storage shape
|
|
117
|
+
|
|
118
|
+
Persist `exportJson` output as your source of truth:
|
|
119
|
+
|
|
120
|
+
- `type: "email_document"`
|
|
121
|
+
- `settings`
|
|
122
|
+
- `rows[]` (preferred export shape)
|
|
123
|
+
- row `layout`, `styles`, `columns[].blocks[]`
|
|
124
|
+
- `_reactEmailStudio` metadata for editor round-trip
|
|
125
|
+
|
|
126
|
+
Import supports legacy `blocks[]` and older styles, but export writes `rows[]`.
|
|
127
|
+
|
|
128
|
+
## Troubleshooting
|
|
129
|
+
|
|
130
|
+
- Missing module errors: install all peers with compatible versions.
|
|
131
|
+
- Blank HTML from `jsonToHtml`: invalid/empty design input.
|
|
132
|
+
- Broken email images: `onUpload` URL is private, local, or expired.
|
|
133
|
+
- SSR errors: ensure client boundary (`"use client"` or dynamic `ssr: false`).
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -864,6 +864,65 @@ function countBlocksInDesign(rows) {
|
|
|
864
864
|
}, 0);
|
|
865
865
|
return rows.reduce((sum, r) => sum + r.cells.reduce((s, c) => s + colBlocks(c), 0), 0);
|
|
866
866
|
}
|
|
867
|
+
function isSingleBlockInRow(row) {
|
|
868
|
+
if (!row?.cells) return false;
|
|
869
|
+
let count = 0;
|
|
870
|
+
for (const col of row.cells) {
|
|
871
|
+
if (!Array.isArray(col)) continue;
|
|
872
|
+
count += col.length;
|
|
873
|
+
if (count > 1) return false;
|
|
874
|
+
}
|
|
875
|
+
return count === 1;
|
|
876
|
+
}
|
|
877
|
+
function resolveBlockMovePlan(rows, rowId, cellIdx, blockIndex, inner) {
|
|
878
|
+
const rowIndex = rows.findIndex((r) => r.id === rowId);
|
|
879
|
+
const row = rowIndex >= 0 ? rows[rowIndex] : void 0;
|
|
880
|
+
if (!row) return null;
|
|
881
|
+
if (inner) {
|
|
882
|
+
const loc = toColumnLoc(rowId, cellIdx, {
|
|
883
|
+
parentBlockIdx: inner.parentBlockIdx,
|
|
884
|
+
innerCellIdx: inner.cellIdx
|
|
885
|
+
});
|
|
886
|
+
const col2 = getColumnBlocks(rows, loc);
|
|
887
|
+
const idx = inner.contentIdx;
|
|
888
|
+
return {
|
|
889
|
+
mode: "column",
|
|
890
|
+
blockIndex: idx,
|
|
891
|
+
canMoveUp: idx > 0,
|
|
892
|
+
canMoveDown: idx < col2.length - 1
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const col = row.cells?.[cellIdx];
|
|
896
|
+
const colLen = Array.isArray(col) ? col.length : 0;
|
|
897
|
+
if (colLen > 1) {
|
|
898
|
+
return {
|
|
899
|
+
mode: "column",
|
|
900
|
+
blockIndex,
|
|
901
|
+
canMoveUp: blockIndex > 0,
|
|
902
|
+
canMoveDown: blockIndex < colLen - 1
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (colLen === 1 && (isRootContentRow(row) || isSingleBlockInRow(row))) {
|
|
906
|
+
return {
|
|
907
|
+
mode: "row",
|
|
908
|
+
canMoveUp: rowIndex > 0,
|
|
909
|
+
canMoveDown: rowIndex < rows.length - 1
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (colLen === 1) {
|
|
913
|
+
return {
|
|
914
|
+
mode: "row",
|
|
915
|
+
canMoveUp: rowIndex > 0,
|
|
916
|
+
canMoveDown: rowIndex < rows.length - 1
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
mode: "column",
|
|
921
|
+
blockIndex,
|
|
922
|
+
canMoveUp: false,
|
|
923
|
+
canMoveDown: false
|
|
924
|
+
};
|
|
925
|
+
}
|
|
867
926
|
|
|
868
927
|
// src/lib/htmlUtils.ts
|
|
869
928
|
function escHtmlAttr(s) {
|
|
@@ -872,6 +931,11 @@ function escHtmlAttr(s) {
|
|
|
872
931
|
function escHtml(s) {
|
|
873
932
|
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
874
933
|
}
|
|
934
|
+
function textBlockContentToHtml(content) {
|
|
935
|
+
const raw = typeof content === "string" ? content : "";
|
|
936
|
+
if (/^\s*<[^>]+>/.test(raw)) return raw;
|
|
937
|
+
return escHtml(raw).replace(/\r?\n/g, "<br/>");
|
|
938
|
+
}
|
|
875
939
|
function utf8ToBase64(raw) {
|
|
876
940
|
const bytes = new TextEncoder().encode(raw);
|
|
877
941
|
let bin = "";
|
|
@@ -1212,8 +1276,7 @@ function blockToHtml(cb) {
|
|
|
1212
1276
|
case "text": {
|
|
1213
1277
|
const shell = emailSurfaceBgCss(p);
|
|
1214
1278
|
const inner = `font-size:${lenPx(p.fontSize)};color:${p.color};text-align:${p.align};font-weight:${p.fontWeight || 400};font-style:${p.italic ? "italic" : "normal"};text-decoration:${p.underline ? "underline" : "none"};line-height:${lh(p.lineHeight)};letter-spacing:${lenPx(p.letterSpacing)};font-family:${p.fontFamily || "Georgia,serif"}`;
|
|
1215
|
-
const
|
|
1216
|
-
const body = /^\s*<[^>]+>/.test(content) ? content : escHtml(content).replace(/\r?\n/g, "<br/>");
|
|
1279
|
+
const body = textBlockContentToHtml(p.content);
|
|
1217
1280
|
return `<div style="${pd(p.padding)};${marginCss()}${shell}"><div style="${inner}">${body}</div></div>`;
|
|
1218
1281
|
}
|
|
1219
1282
|
case "html": {
|
|
@@ -3052,7 +3115,7 @@ function ContentBlock({ block, selected, onClick, preview, C }) {
|
|
|
3052
3115
|
"div",
|
|
3053
3116
|
{
|
|
3054
3117
|
style: { fontSize: p.fontSize, color: p.color, textAlign: p.align, fontWeight: p.fontWeight || (p.bold ? 700 : 400), fontStyle: p.italic ? "italic" : "normal", textDecoration: p.underline ? "underline" : "none", lineHeight: p.lineHeight || 1.65, letterSpacing: `${p.letterSpacing || 0}px`, fontFamily: p.fontFamily || "Georgia,serif" },
|
|
3055
|
-
dangerouslySetInnerHTML: { __html: p.content }
|
|
3118
|
+
dangerouslySetInnerHTML: { __html: textBlockContentToHtml(p.content) }
|
|
3056
3119
|
}
|
|
3057
3120
|
)
|
|
3058
3121
|
);
|
|
@@ -7424,50 +7487,64 @@ var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
|
7424
7487
|
function ContextMenu({ x, y, items, onClose, C }) {
|
|
7425
7488
|
const ref = (0, import_react6.useRef)(null);
|
|
7426
7489
|
(0, import_react6.useEffect)(() => {
|
|
7427
|
-
const
|
|
7428
|
-
|
|
7429
|
-
|
|
7490
|
+
const onPointerDown = (ev) => {
|
|
7491
|
+
if (ref.current?.contains(ev.target)) return;
|
|
7492
|
+
onClose();
|
|
7493
|
+
};
|
|
7494
|
+
const onKeyDown = (ev) => {
|
|
7495
|
+
if (ev.key === "Escape") onClose();
|
|
7496
|
+
};
|
|
7497
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
7498
|
+
window.addEventListener("keydown", onKeyDown);
|
|
7430
7499
|
return () => {
|
|
7431
|
-
window.removeEventListener("
|
|
7432
|
-
window.removeEventListener("keydown",
|
|
7500
|
+
window.removeEventListener("pointerdown", onPointerDown);
|
|
7501
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
7433
7502
|
};
|
|
7434
7503
|
}, [onClose]);
|
|
7435
|
-
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
7436
|
-
"
|
|
7504
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
7505
|
+
"div",
|
|
7437
7506
|
{
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7507
|
+
ref,
|
|
7508
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
7509
|
+
style: { position: "fixed", left: x, top: y, zIndex: 9999, background: C.sidebar, border: `1px solid ${C.border}`, borderRadius: 7, padding: "4px 0", boxShadow: `0 8px 32px rgba(0,0,0,.4)`, minWidth: 160 },
|
|
7510
|
+
children: items.map((item, i) => item.sep ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { height: 1, background: C.border, margin: "3px 0" } }, i) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
7511
|
+
"button",
|
|
7512
|
+
{
|
|
7513
|
+
type: "button",
|
|
7514
|
+
disabled: !!item.disabled,
|
|
7515
|
+
onClick: () => {
|
|
7516
|
+
if (item.disabled) return;
|
|
7517
|
+
item.action();
|
|
7518
|
+
onClose();
|
|
7519
|
+
},
|
|
7520
|
+
style: {
|
|
7521
|
+
display: "block",
|
|
7522
|
+
width: "100%",
|
|
7523
|
+
background: "none",
|
|
7524
|
+
border: "none",
|
|
7525
|
+
padding: "7px 14px",
|
|
7526
|
+
textAlign: "left",
|
|
7527
|
+
cursor: item.disabled ? "not-allowed" : "pointer",
|
|
7528
|
+
fontSize: 12,
|
|
7529
|
+
color: item.disabled ? C.muted : item.danger ? C.danger : C.text,
|
|
7530
|
+
fontWeight: item.bold ? 700 : 400,
|
|
7531
|
+
opacity: item.disabled ? 0.45 : 1
|
|
7532
|
+
},
|
|
7533
|
+
onMouseEnter: (e) => {
|
|
7534
|
+
if (!item.disabled) e.currentTarget.style.background = C.surface;
|
|
7535
|
+
},
|
|
7536
|
+
onMouseLeave: (e) => {
|
|
7537
|
+
e.currentTarget.style.background = "none";
|
|
7538
|
+
},
|
|
7539
|
+
children: [
|
|
7540
|
+
item.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { marginRight: 8 }, children: item.icon }),
|
|
7541
|
+
item.label
|
|
7542
|
+
]
|
|
7543
|
+
},
|
|
7544
|
+
i
|
|
7545
|
+
))
|
|
7546
|
+
}
|
|
7547
|
+
);
|
|
7471
7548
|
}
|
|
7472
7549
|
|
|
7473
7550
|
// src/editor/overlays/ConfirmDialog.tsx
|
|
@@ -8071,10 +8148,27 @@ var ReactEmailEditorComponent = (0, import_react8.forwardRef)(
|
|
|
8071
8148
|
else dropContent(rowId, cellIdx, { kind: "duplicate", fromIdx: contentIdx });
|
|
8072
8149
|
}
|
|
8073
8150
|
};
|
|
8151
|
+
const shiftBlockFromMenu = (rowId, cellIdx, ci, dir, inner = null) => {
|
|
8152
|
+
const blockIndex = inner ? inner.contentIdx : ci;
|
|
8153
|
+
const plan = resolveBlockMovePlan(rowsRef.current, rowId, cellIdx, blockIndex, inner);
|
|
8154
|
+
if (!plan) return;
|
|
8155
|
+
if (dir < 0 && !plan.canMoveUp || dir > 0 && !plan.canMoveDown) return;
|
|
8156
|
+
if (plan.mode === "row") {
|
|
8157
|
+
moveRow(rowId, dir);
|
|
8158
|
+
return;
|
|
8159
|
+
}
|
|
8160
|
+
moveContent(rowId, cellIdx, plan.blockIndex, dir, inner);
|
|
8161
|
+
};
|
|
8074
8162
|
const handleContextMenu = (e, rowId, cellIdx, ci, inner = null) => {
|
|
8163
|
+
e.preventDefault();
|
|
8164
|
+
e.stopPropagation();
|
|
8165
|
+
const blockIndex = inner ? inner.contentIdx : ci;
|
|
8166
|
+
const plan = resolveBlockMovePlan(rowsRef.current, rowId, cellIdx, blockIndex, inner);
|
|
8167
|
+
const canMoveUp = plan?.canMoveUp ?? false;
|
|
8168
|
+
const canMoveDown = plan?.canMoveDown ?? false;
|
|
8075
8169
|
setCtxMenu({ x: e.clientX, y: e.clientY, items: [
|
|
8076
|
-
{ icon: "\u2191", label: "Move Up", action: () =>
|
|
8077
|
-
{ icon: "\u2193", label: "Move Down", action: () =>
|
|
8170
|
+
{ icon: "\u2191", label: "Move Up", disabled: !canMoveUp, action: () => shiftBlockFromMenu(rowId, cellIdx, ci, -1, inner) },
|
|
8171
|
+
{ icon: "\u2193", label: "Move Down", disabled: !canMoveDown, action: () => shiftBlockFromMenu(rowId, cellIdx, ci, 1, inner) },
|
|
8078
8172
|
{ sep: true },
|
|
8079
8173
|
{ icon: "\u29C9", label: "Duplicate", action: () => dropContent(rowId, cellIdx, inner ? { kind: "duplicate", fromIdx: inner.contentIdx, fromNested: { parentBlockIdx: inner.parentBlockIdx, innerCellIdx: inner.cellIdx } } : { kind: "duplicate", fromIdx: ci }) },
|
|
8080
8174
|
{ sep: true },
|
|
@@ -8084,17 +8178,14 @@ var ReactEmailEditorComponent = (0, import_react8.forwardRef)(
|
|
|
8084
8178
|
const handleLayoutContextMenu = (e, rowId) => {
|
|
8085
8179
|
e.preventDefault();
|
|
8086
8180
|
e.stopPropagation();
|
|
8087
|
-
const
|
|
8181
|
+
const currentRows = rowsRef.current;
|
|
8182
|
+
const ri = currentRows.findIndex((r) => r.id === rowId);
|
|
8088
8183
|
setSelectedRowId(rowId);
|
|
8089
8184
|
setSelContentId(null);
|
|
8090
8185
|
setSelMeta(null);
|
|
8091
8186
|
setCtxMenu({ x: e.clientX, y: e.clientY, items: [
|
|
8092
|
-
{ icon: "\u2191", label: "Move row up", disabled: ri <= 0, action: () =>
|
|
8093
|
-
|
|
8094
|
-
} },
|
|
8095
|
-
{ icon: "\u2193", label: "Move row down", disabled: ri < 0 || ri >= rows.length - 1, action: () => {
|
|
8096
|
-
if (ri >= 0 && ri < rows.length - 1) moveRow(rowId, 1);
|
|
8097
|
-
} },
|
|
8187
|
+
{ icon: "\u2191", label: "Move row up", disabled: ri <= 0, action: () => moveRow(rowId, -1) },
|
|
8188
|
+
{ icon: "\u2193", label: "Move row down", disabled: ri < 0 || ri >= currentRows.length - 1, action: () => moveRow(rowId, 1) },
|
|
8098
8189
|
{ sep: true },
|
|
8099
8190
|
{ icon: "\u29C9", label: "Duplicate row", action: () => dupRow(rowId) },
|
|
8100
8191
|
{ sep: true },
|