@particle-academy/fancy-cms-ui 0.0.1-dev.7 → 0.0.1-dev.8
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/dist/editor.cjs +248 -4
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +213 -2
- package/dist/editor.js.map +1 -1
- package/package.json +2 -1
package/dist/editor.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var reactFancy = require('@particle-academy/react-fancy');
|
|
5
6
|
|
|
6
7
|
// src/editor/Editor.tsx
|
|
7
8
|
|
|
@@ -725,11 +726,44 @@ function Toolbar({ ed }) {
|
|
|
725
726
|
var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
726
727
|
var RADIX = BASE.length;
|
|
727
728
|
var MID = Math.floor(RADIX / 2);
|
|
729
|
+
function val(c) {
|
|
730
|
+
const i = BASE.indexOf(c);
|
|
731
|
+
return i < 0 ? 0 : i;
|
|
732
|
+
}
|
|
728
733
|
function keyBetween(a, b) {
|
|
729
734
|
if (a === null) a = "";
|
|
730
|
-
{
|
|
735
|
+
if (b === null) {
|
|
731
736
|
return a + BASE[MID];
|
|
732
737
|
}
|
|
738
|
+
if (a >= b) {
|
|
739
|
+
throw new Error(`keyBetween: lower bound "${a}" is not below upper bound "${b}"`);
|
|
740
|
+
}
|
|
741
|
+
let result = "";
|
|
742
|
+
let i = 0;
|
|
743
|
+
for (; ; ) {
|
|
744
|
+
const ca = i < a.length ? val(a[i]) : 0;
|
|
745
|
+
const cb = i < b.length ? val(b[i]) : RADIX;
|
|
746
|
+
if (cb - ca > 1) {
|
|
747
|
+
result += BASE[ca + Math.floor((cb - ca) / 2)];
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
if (ca === cb) {
|
|
751
|
+
result += BASE[ca];
|
|
752
|
+
i++;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
result += BASE[ca];
|
|
756
|
+
i++;
|
|
757
|
+
for (; ; ) {
|
|
758
|
+
const da = i < a.length ? val(a[i]) : 0;
|
|
759
|
+
if (RADIX - da > 1) {
|
|
760
|
+
result += BASE[da + Math.floor((RADIX - da) / 2)];
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
result += BASE[da];
|
|
764
|
+
i++;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
733
767
|
}
|
|
734
768
|
var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
|
|
735
769
|
function NodeInspector({
|
|
@@ -926,6 +960,110 @@ var input2 = {
|
|
|
926
960
|
};
|
|
927
961
|
var iconBtn = { background: "transparent", border: "none", color: "#e2e8f0", cursor: "pointer", fontSize: 13, padding: 4, borderRadius: 6 };
|
|
928
962
|
var miniBtn = { font: "inherit", fontSize: 11, color: "#e2e8f0", background: "#334155", border: "1px solid #475569", borderRadius: 6, padding: "5px 10px", cursor: "pointer" };
|
|
963
|
+
|
|
964
|
+
// src/editor/editorOps.ts
|
|
965
|
+
function childrenOfMap(nodes, parent) {
|
|
966
|
+
return Object.values(nodes).filter((n) => n.parent === parent).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
|
|
967
|
+
}
|
|
968
|
+
function subtreeBfs(nodes, root) {
|
|
969
|
+
const out = [];
|
|
970
|
+
const queue = [root];
|
|
971
|
+
while (queue.length) {
|
|
972
|
+
const cur = queue.shift();
|
|
973
|
+
out.push(cur);
|
|
974
|
+
for (const child of childrenOfMap(nodes, cur)) queue.push(child.id);
|
|
975
|
+
}
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
function cloneFrom(nodes, rootId, newParent, newOrder, seqBase) {
|
|
979
|
+
if (!nodes[rootId]) return { ops: [], newRootId: null };
|
|
980
|
+
const order = subtreeBfs(nodes, rootId);
|
|
981
|
+
const idMap = {};
|
|
982
|
+
order.forEach((old, i) => idMap[old] = `${old}_c${seqBase}_${i}`);
|
|
983
|
+
const ops = order.map((old) => {
|
|
984
|
+
const src = nodes[old];
|
|
985
|
+
const cloned = JSON.parse(JSON.stringify(src));
|
|
986
|
+
cloned.id = idMap[old];
|
|
987
|
+
cloned.parent = old === rootId ? newParent : idMap[src.parent];
|
|
988
|
+
if (old === rootId) cloned.order = newOrder;
|
|
989
|
+
return { t: "insert_node", node: cloned };
|
|
990
|
+
});
|
|
991
|
+
return { ops, newRootId: idMap[rootId] };
|
|
992
|
+
}
|
|
993
|
+
function duplicateOps(doc, id) {
|
|
994
|
+
const node = doc.nodes[id];
|
|
995
|
+
if (!node) return { ops: [], newRootId: null };
|
|
996
|
+
const sibs = childrenOf(doc, node.parent);
|
|
997
|
+
const idx = sibs.findIndex((s) => s.id === id);
|
|
998
|
+
const newOrder = keyBetween(node.order, sibs[idx + 1]?.order ?? null);
|
|
999
|
+
const { ops, newRootId } = cloneFrom(doc.nodes, id, node.parent, newOrder, doc.seq);
|
|
1000
|
+
if (node.parent === null && newRootId) {
|
|
1001
|
+
const base = doc.sections;
|
|
1002
|
+
const at = base.indexOf(id);
|
|
1003
|
+
ops.push({ t: "reorder_sections", order: [...base.slice(0, at + 1), newRootId, ...base.slice(at + 1)] });
|
|
1004
|
+
}
|
|
1005
|
+
return { ops, newRootId };
|
|
1006
|
+
}
|
|
1007
|
+
function pasteOps(doc, clip, target, containerTypes) {
|
|
1008
|
+
const sel = target ? doc.nodes[target] : null;
|
|
1009
|
+
const parent = sel ? containerTypes.has(sel.type) ? sel.id : sel.parent : doc.sections[doc.sections.length - 1] ?? null;
|
|
1010
|
+
const sibs = childrenOf(doc, parent);
|
|
1011
|
+
const newOrder = keyBetween(sibs.length ? sibs[sibs.length - 1].order : null, null);
|
|
1012
|
+
const { ops, newRootId } = cloneFrom(clip.nodes, clip.rootId, parent, newOrder, doc.seq);
|
|
1013
|
+
return { ops, newRootId };
|
|
1014
|
+
}
|
|
1015
|
+
function reorderOps(doc, id, dir) {
|
|
1016
|
+
const node = doc.nodes[id];
|
|
1017
|
+
if (!node) return [];
|
|
1018
|
+
if (node.parent === null) {
|
|
1019
|
+
const s = [...doc.sections];
|
|
1020
|
+
const i = s.indexOf(id);
|
|
1021
|
+
if (i < 0) return [];
|
|
1022
|
+
s.splice(i, 1);
|
|
1023
|
+
if (dir === "front") s.unshift(id);
|
|
1024
|
+
else if (dir === "back") s.push(id);
|
|
1025
|
+
else s.splice(Math.max(0, Math.min(s.length, dir === "up" ? i - 1 : i + 1)), 0, id);
|
|
1026
|
+
return [{ t: "reorder_sections", order: s }];
|
|
1027
|
+
}
|
|
1028
|
+
const sibs = childrenOf(doc, node.parent);
|
|
1029
|
+
const idx = sibs.findIndex((sb) => sb.id === id);
|
|
1030
|
+
if (idx < 0) return [];
|
|
1031
|
+
const last = sibs.length - 1;
|
|
1032
|
+
let newOrder;
|
|
1033
|
+
if (dir === "up") {
|
|
1034
|
+
if (idx <= 0) return [];
|
|
1035
|
+
newOrder = keyBetween(sibs[idx - 2]?.order ?? null, sibs[idx - 1].order);
|
|
1036
|
+
} else if (dir === "down") {
|
|
1037
|
+
if (idx >= last) return [];
|
|
1038
|
+
newOrder = keyBetween(sibs[idx + 1].order, sibs[idx + 2]?.order ?? null);
|
|
1039
|
+
} else if (dir === "front") {
|
|
1040
|
+
if (idx >= last) return [];
|
|
1041
|
+
newOrder = keyBetween(sibs[last].order, null);
|
|
1042
|
+
} else {
|
|
1043
|
+
if (idx <= 0) return [];
|
|
1044
|
+
newOrder = keyBetween(null, sibs[0].order);
|
|
1045
|
+
}
|
|
1046
|
+
return [{ t: "move_node", id, parent: node.parent, order: newOrder }];
|
|
1047
|
+
}
|
|
1048
|
+
function wrapInBoxOps(doc, id) {
|
|
1049
|
+
const node = doc.nodes[id];
|
|
1050
|
+
if (!node || node.parent === null) return { ops: [], newRootId: null };
|
|
1051
|
+
const frameId = `box_${doc.seq}`;
|
|
1052
|
+
return {
|
|
1053
|
+
ops: [
|
|
1054
|
+
{ t: "insert_node", node: { id: frameId, type: "frame", parent: node.parent, order: node.order, props: {}, style: { base: { padding: { value: 16, unit: "px" } } } } },
|
|
1055
|
+
{ t: "move_node", id, parent: frameId, order: "a" }
|
|
1056
|
+
],
|
|
1057
|
+
newRootId: frameId
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function snapshotSubtree(doc, id) {
|
|
1061
|
+
if (!doc.nodes[id]) return null;
|
|
1062
|
+
const ids = subtreeBfs(doc.nodes, id);
|
|
1063
|
+
const nodes = {};
|
|
1064
|
+
for (const nid of ids) nodes[nid] = JSON.parse(JSON.stringify(doc.nodes[nid]));
|
|
1065
|
+
return { rootId: id, nodes };
|
|
1066
|
+
}
|
|
929
1067
|
var Z2 = 2147483e3;
|
|
930
1068
|
function EditablePage({
|
|
931
1069
|
doc,
|
|
@@ -1029,7 +1167,7 @@ function EditablePage({
|
|
|
1029
1167
|
const doc2 = ed.state.doc;
|
|
1030
1168
|
const parent = selNode ? CONTAINER_TYPES2.has(selNode.type) ? selNode.id : selNode.parent : doc2.sections[doc2.sections.length - 1] ?? null;
|
|
1031
1169
|
const siblings = childrenOf(doc2, parent);
|
|
1032
|
-
const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null);
|
|
1170
|
+
const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
|
|
1033
1171
|
const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
|
|
1034
1172
|
const def = ADD_DEFAULTS[kind];
|
|
1035
1173
|
ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
|
|
@@ -1044,9 +1182,76 @@ function EditablePage({
|
|
|
1044
1182
|
ed.apply({ t: "remove_node", id: selection });
|
|
1045
1183
|
ed.select(null);
|
|
1046
1184
|
};
|
|
1047
|
-
const
|
|
1185
|
+
const clip = react.useRef(null);
|
|
1186
|
+
const runOps = (result) => {
|
|
1187
|
+
result.ops.forEach((op) => ed.apply(op));
|
|
1188
|
+
if (result.newRootId) ed.select(result.newRootId);
|
|
1189
|
+
};
|
|
1190
|
+
const duplicate = (id) => id && runOps(duplicateOps(ed.state.doc, id));
|
|
1191
|
+
const reorder = (id, dir) => id && reorderOps(ed.state.doc, id, dir).forEach((op) => ed.apply(op));
|
|
1192
|
+
const wrapInBox = (id) => id && runOps(wrapInBoxOps(ed.state.doc, id));
|
|
1193
|
+
const copy = (id) => {
|
|
1194
|
+
if (id) clip.current = snapshotSubtree(ed.state.doc, id);
|
|
1195
|
+
};
|
|
1196
|
+
const paste = (target) => {
|
|
1197
|
+
if (clip.current) runOps(pasteOps(ed.state.doc, clip.current, target, CONTAINER_TYPES2));
|
|
1198
|
+
};
|
|
1199
|
+
const startEditText = (id) => {
|
|
1200
|
+
if (!id) return;
|
|
1201
|
+
ed.select(id);
|
|
1202
|
+
requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
|
|
1203
|
+
};
|
|
1204
|
+
react.useEffect(() => {
|
|
1205
|
+
if (!editing) return;
|
|
1206
|
+
const onKey = (e) => {
|
|
1207
|
+
const t = e.target;
|
|
1208
|
+
const typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
|
|
1209
|
+
if (e.key === "Escape") return void ed.select(null);
|
|
1210
|
+
if (typing) return;
|
|
1211
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selection) {
|
|
1212
|
+
e.preventDefault();
|
|
1213
|
+
removeSelected();
|
|
1214
|
+
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d" && selection) {
|
|
1215
|
+
e.preventDefault();
|
|
1216
|
+
duplicate(selection);
|
|
1217
|
+
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c" && selection) {
|
|
1218
|
+
copy(selection);
|
|
1219
|
+
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
|
|
1220
|
+
e.preventDefault();
|
|
1221
|
+
paste(selection);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
window.addEventListener("keydown", onKey);
|
|
1225
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1226
|
+
}, [editing, selection]);
|
|
1227
|
+
const onContextMenuSelect = (e) => {
|
|
1228
|
+
if (!editing) return;
|
|
1229
|
+
const el = e.target.closest("[data-cms]");
|
|
1230
|
+
if (el) ed.select(el.getAttribute("data-cms"));
|
|
1231
|
+
else ed.select(null);
|
|
1232
|
+
};
|
|
1233
|
+
const page = /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) });
|
|
1234
|
+
const pageShell = pinned ? /* @__PURE__ */ jsxRuntime.jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : page;
|
|
1048
1235
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
|
|
1049
|
-
|
|
1236
|
+
editing ? /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu, { children: [
|
|
1237
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Trigger, { children: pageShell }),
|
|
1238
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Content, { className: "!z-[2147483646]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1239
|
+
EditMenuItems,
|
|
1240
|
+
{
|
|
1241
|
+
node: selNode ?? null,
|
|
1242
|
+
hasClipboard: Boolean(clip.current),
|
|
1243
|
+
isContainer: selNode ? CONTAINER_TYPES2.has(selNode.type) : false,
|
|
1244
|
+
onEditText: () => startEditText(selection),
|
|
1245
|
+
onAdd: addNode,
|
|
1246
|
+
onDuplicate: () => duplicate(selection),
|
|
1247
|
+
onCopy: () => copy(selection),
|
|
1248
|
+
onPaste: () => paste(selection),
|
|
1249
|
+
onWrap: () => wrapInBox(selection),
|
|
1250
|
+
onReorder: (d) => reorder(selection, d),
|
|
1251
|
+
onDelete: removeSelected
|
|
1252
|
+
}
|
|
1253
|
+
) })
|
|
1254
|
+
] }) : pageShell,
|
|
1050
1255
|
editing && box && selection ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1051
1256
|
SelectionOverlay,
|
|
1052
1257
|
{
|
|
@@ -1300,6 +1505,45 @@ function EditBar({
|
|
|
1300
1505
|
] }) : null
|
|
1301
1506
|
] });
|
|
1302
1507
|
}
|
|
1508
|
+
function EditMenuItems({
|
|
1509
|
+
node,
|
|
1510
|
+
hasClipboard,
|
|
1511
|
+
isContainer,
|
|
1512
|
+
onEditText,
|
|
1513
|
+
onAdd,
|
|
1514
|
+
onDuplicate,
|
|
1515
|
+
onCopy,
|
|
1516
|
+
onPaste,
|
|
1517
|
+
onWrap,
|
|
1518
|
+
onReorder,
|
|
1519
|
+
onDelete
|
|
1520
|
+
}) {
|
|
1521
|
+
const editable = node && (node.type === "text" || node.type === "heading");
|
|
1522
|
+
const nested = Boolean(node && node.parent !== null);
|
|
1523
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1524
|
+
editable ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
|
|
1525
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu.Sub, { children: [
|
|
1526
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
|
|
1527
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubContent, { className: "!z-[2147483647]", children: ADD_MENU.map((m) => /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onAdd(m.kind), children: m.label }, m.kind)) })
|
|
1528
|
+
] }),
|
|
1529
|
+
node ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1530
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
|
|
1531
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
|
|
1532
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
|
|
1533
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" }),
|
|
1534
|
+
nested ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onWrap, children: "Wrap in Box" }) : null,
|
|
1535
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
|
|
1536
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
|
|
1537
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
|
|
1538
|
+
nested ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1539
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
|
|
1540
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
|
|
1541
|
+
] }) : null,
|
|
1542
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
|
|
1543
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { danger: true, onClick: onDelete, children: "Delete" })
|
|
1544
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" })
|
|
1545
|
+
] });
|
|
1546
|
+
}
|
|
1303
1547
|
function applyTransform(el, t) {
|
|
1304
1548
|
if (t && (t.x != null || t.y != null || t.scale != null || t.rotate != null || t.opacity != null || t.w != null || t.h != null)) {
|
|
1305
1549
|
el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1}) rotate(${t.rotate ?? 0}deg)`;
|