react-email-studio 3.8.2 → 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 +7 -0
- package/README.md +3 -1
- package/dist/index.cjs +136 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +136 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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
|
+
|
|
8
15
|
## [3.8.2] - 2026-05-22
|
|
9
16
|
|
|
10
17
|
### Added
|
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
|
|
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) {
|
|
@@ -7428,50 +7487,64 @@ var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
|
7428
7487
|
function ContextMenu({ x, y, items, onClose, C }) {
|
|
7429
7488
|
const ref = (0, import_react6.useRef)(null);
|
|
7430
7489
|
(0, import_react6.useEffect)(() => {
|
|
7431
|
-
const
|
|
7432
|
-
|
|
7433
|
-
|
|
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);
|
|
7434
7499
|
return () => {
|
|
7435
|
-
window.removeEventListener("
|
|
7436
|
-
window.removeEventListener("keydown",
|
|
7500
|
+
window.removeEventListener("pointerdown", onPointerDown);
|
|
7501
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
7437
7502
|
};
|
|
7438
7503
|
}, [onClose]);
|
|
7439
|
-
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
7440
|
-
"
|
|
7504
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
7505
|
+
"div",
|
|
7441
7506
|
{
|
|
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
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
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
|
+
);
|
|
7475
7548
|
}
|
|
7476
7549
|
|
|
7477
7550
|
// src/editor/overlays/ConfirmDialog.tsx
|
|
@@ -8075,10 +8148,27 @@ var ReactEmailEditorComponent = (0, import_react8.forwardRef)(
|
|
|
8075
8148
|
else dropContent(rowId, cellIdx, { kind: "duplicate", fromIdx: contentIdx });
|
|
8076
8149
|
}
|
|
8077
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
|
+
};
|
|
8078
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;
|
|
8079
8169
|
setCtxMenu({ x: e.clientX, y: e.clientY, items: [
|
|
8080
|
-
{ icon: "\u2191", label: "Move Up", action: () =>
|
|
8081
|
-
{ 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) },
|
|
8082
8172
|
{ sep: true },
|
|
8083
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 }) },
|
|
8084
8174
|
{ sep: true },
|
|
@@ -8088,17 +8178,14 @@ var ReactEmailEditorComponent = (0, import_react8.forwardRef)(
|
|
|
8088
8178
|
const handleLayoutContextMenu = (e, rowId) => {
|
|
8089
8179
|
e.preventDefault();
|
|
8090
8180
|
e.stopPropagation();
|
|
8091
|
-
const
|
|
8181
|
+
const currentRows = rowsRef.current;
|
|
8182
|
+
const ri = currentRows.findIndex((r) => r.id === rowId);
|
|
8092
8183
|
setSelectedRowId(rowId);
|
|
8093
8184
|
setSelContentId(null);
|
|
8094
8185
|
setSelMeta(null);
|
|
8095
8186
|
setCtxMenu({ x: e.clientX, y: e.clientY, items: [
|
|
8096
|
-
{ icon: "\u2191", label: "Move row up", disabled: ri <= 0, action: () =>
|
|
8097
|
-
|
|
8098
|
-
} },
|
|
8099
|
-
{ icon: "\u2193", label: "Move row down", disabled: ri < 0 || ri >= rows.length - 1, action: () => {
|
|
8100
|
-
if (ri >= 0 && ri < rows.length - 1) moveRow(rowId, 1);
|
|
8101
|
-
} },
|
|
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) },
|
|
8102
8189
|
{ sep: true },
|
|
8103
8190
|
{ icon: "\u29C9", label: "Duplicate row", action: () => dupRow(rowId) },
|
|
8104
8191
|
{ sep: true },
|