@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.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CmsPage, defaultRegistry } from './chunk-5O6U4VIU.js';
|
|
|
3
3
|
import { reduce, childrenOf } from './chunk-HYHNMW47.js';
|
|
4
4
|
import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useEffect } from 'react';
|
|
5
5
|
import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
6
|
+
import { ContextMenu } from '@particle-academy/react-fancy';
|
|
6
7
|
|
|
7
8
|
function initEditor(doc) {
|
|
8
9
|
return { doc, past: [], future: [], selection: null };
|
|
@@ -559,6 +560,110 @@ var input2 = {
|
|
|
559
560
|
};
|
|
560
561
|
var iconBtn = { background: "transparent", border: "none", color: "#e2e8f0", cursor: "pointer", fontSize: 13, padding: 4, borderRadius: 6 };
|
|
561
562
|
var miniBtn = { font: "inherit", fontSize: 11, color: "#e2e8f0", background: "#334155", border: "1px solid #475569", borderRadius: 6, padding: "5px 10px", cursor: "pointer" };
|
|
563
|
+
|
|
564
|
+
// src/editor/editorOps.ts
|
|
565
|
+
function childrenOfMap(nodes, parent) {
|
|
566
|
+
return Object.values(nodes).filter((n) => n.parent === parent).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
|
|
567
|
+
}
|
|
568
|
+
function subtreeBfs(nodes, root) {
|
|
569
|
+
const out = [];
|
|
570
|
+
const queue = [root];
|
|
571
|
+
while (queue.length) {
|
|
572
|
+
const cur = queue.shift();
|
|
573
|
+
out.push(cur);
|
|
574
|
+
for (const child of childrenOfMap(nodes, cur)) queue.push(child.id);
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
function cloneFrom(nodes, rootId, newParent, newOrder, seqBase) {
|
|
579
|
+
if (!nodes[rootId]) return { ops: [], newRootId: null };
|
|
580
|
+
const order = subtreeBfs(nodes, rootId);
|
|
581
|
+
const idMap = {};
|
|
582
|
+
order.forEach((old, i) => idMap[old] = `${old}_c${seqBase}_${i}`);
|
|
583
|
+
const ops = order.map((old) => {
|
|
584
|
+
const src = nodes[old];
|
|
585
|
+
const cloned = JSON.parse(JSON.stringify(src));
|
|
586
|
+
cloned.id = idMap[old];
|
|
587
|
+
cloned.parent = old === rootId ? newParent : idMap[src.parent];
|
|
588
|
+
if (old === rootId) cloned.order = newOrder;
|
|
589
|
+
return { t: "insert_node", node: cloned };
|
|
590
|
+
});
|
|
591
|
+
return { ops, newRootId: idMap[rootId] };
|
|
592
|
+
}
|
|
593
|
+
function duplicateOps(doc, id) {
|
|
594
|
+
const node = doc.nodes[id];
|
|
595
|
+
if (!node) return { ops: [], newRootId: null };
|
|
596
|
+
const sibs = childrenOf(doc, node.parent);
|
|
597
|
+
const idx = sibs.findIndex((s) => s.id === id);
|
|
598
|
+
const newOrder = keyBetween(node.order, sibs[idx + 1]?.order ?? null);
|
|
599
|
+
const { ops, newRootId } = cloneFrom(doc.nodes, id, node.parent, newOrder, doc.seq);
|
|
600
|
+
if (node.parent === null && newRootId) {
|
|
601
|
+
const base = doc.sections;
|
|
602
|
+
const at = base.indexOf(id);
|
|
603
|
+
ops.push({ t: "reorder_sections", order: [...base.slice(0, at + 1), newRootId, ...base.slice(at + 1)] });
|
|
604
|
+
}
|
|
605
|
+
return { ops, newRootId };
|
|
606
|
+
}
|
|
607
|
+
function pasteOps(doc, clip, target, containerTypes) {
|
|
608
|
+
const sel = target ? doc.nodes[target] : null;
|
|
609
|
+
const parent = sel ? containerTypes.has(sel.type) ? sel.id : sel.parent : doc.sections[doc.sections.length - 1] ?? null;
|
|
610
|
+
const sibs = childrenOf(doc, parent);
|
|
611
|
+
const newOrder = keyBetween(sibs.length ? sibs[sibs.length - 1].order : null, null);
|
|
612
|
+
const { ops, newRootId } = cloneFrom(clip.nodes, clip.rootId, parent, newOrder, doc.seq);
|
|
613
|
+
return { ops, newRootId };
|
|
614
|
+
}
|
|
615
|
+
function reorderOps(doc, id, dir) {
|
|
616
|
+
const node = doc.nodes[id];
|
|
617
|
+
if (!node) return [];
|
|
618
|
+
if (node.parent === null) {
|
|
619
|
+
const s = [...doc.sections];
|
|
620
|
+
const i = s.indexOf(id);
|
|
621
|
+
if (i < 0) return [];
|
|
622
|
+
s.splice(i, 1);
|
|
623
|
+
if (dir === "front") s.unshift(id);
|
|
624
|
+
else if (dir === "back") s.push(id);
|
|
625
|
+
else s.splice(Math.max(0, Math.min(s.length, dir === "up" ? i - 1 : i + 1)), 0, id);
|
|
626
|
+
return [{ t: "reorder_sections", order: s }];
|
|
627
|
+
}
|
|
628
|
+
const sibs = childrenOf(doc, node.parent);
|
|
629
|
+
const idx = sibs.findIndex((sb) => sb.id === id);
|
|
630
|
+
if (idx < 0) return [];
|
|
631
|
+
const last = sibs.length - 1;
|
|
632
|
+
let newOrder;
|
|
633
|
+
if (dir === "up") {
|
|
634
|
+
if (idx <= 0) return [];
|
|
635
|
+
newOrder = keyBetween(sibs[idx - 2]?.order ?? null, sibs[idx - 1].order);
|
|
636
|
+
} else if (dir === "down") {
|
|
637
|
+
if (idx >= last) return [];
|
|
638
|
+
newOrder = keyBetween(sibs[idx + 1].order, sibs[idx + 2]?.order ?? null);
|
|
639
|
+
} else if (dir === "front") {
|
|
640
|
+
if (idx >= last) return [];
|
|
641
|
+
newOrder = keyBetween(sibs[last].order, null);
|
|
642
|
+
} else {
|
|
643
|
+
if (idx <= 0) return [];
|
|
644
|
+
newOrder = keyBetween(null, sibs[0].order);
|
|
645
|
+
}
|
|
646
|
+
return [{ t: "move_node", id, parent: node.parent, order: newOrder }];
|
|
647
|
+
}
|
|
648
|
+
function wrapInBoxOps(doc, id) {
|
|
649
|
+
const node = doc.nodes[id];
|
|
650
|
+
if (!node || node.parent === null) return { ops: [], newRootId: null };
|
|
651
|
+
const frameId = `box_${doc.seq}`;
|
|
652
|
+
return {
|
|
653
|
+
ops: [
|
|
654
|
+
{ t: "insert_node", node: { id: frameId, type: "frame", parent: node.parent, order: node.order, props: {}, style: { base: { padding: { value: 16, unit: "px" } } } } },
|
|
655
|
+
{ t: "move_node", id, parent: frameId, order: "a" }
|
|
656
|
+
],
|
|
657
|
+
newRootId: frameId
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function snapshotSubtree(doc, id) {
|
|
661
|
+
if (!doc.nodes[id]) return null;
|
|
662
|
+
const ids = subtreeBfs(doc.nodes, id);
|
|
663
|
+
const nodes = {};
|
|
664
|
+
for (const nid of ids) nodes[nid] = JSON.parse(JSON.stringify(doc.nodes[nid]));
|
|
665
|
+
return { rootId: id, nodes };
|
|
666
|
+
}
|
|
562
667
|
var Z2 = 2147483e3;
|
|
563
668
|
function EditablePage({
|
|
564
669
|
doc,
|
|
@@ -677,9 +782,76 @@ function EditablePage({
|
|
|
677
782
|
ed.apply({ t: "remove_node", id: selection });
|
|
678
783
|
ed.select(null);
|
|
679
784
|
};
|
|
680
|
-
const
|
|
785
|
+
const clip = useRef(null);
|
|
786
|
+
const runOps = (result) => {
|
|
787
|
+
result.ops.forEach((op) => ed.apply(op));
|
|
788
|
+
if (result.newRootId) ed.select(result.newRootId);
|
|
789
|
+
};
|
|
790
|
+
const duplicate = (id) => id && runOps(duplicateOps(ed.state.doc, id));
|
|
791
|
+
const reorder = (id, dir) => id && reorderOps(ed.state.doc, id, dir).forEach((op) => ed.apply(op));
|
|
792
|
+
const wrapInBox = (id) => id && runOps(wrapInBoxOps(ed.state.doc, id));
|
|
793
|
+
const copy = (id) => {
|
|
794
|
+
if (id) clip.current = snapshotSubtree(ed.state.doc, id);
|
|
795
|
+
};
|
|
796
|
+
const paste = (target) => {
|
|
797
|
+
if (clip.current) runOps(pasteOps(ed.state.doc, clip.current, target, CONTAINER_TYPES2));
|
|
798
|
+
};
|
|
799
|
+
const startEditText = (id) => {
|
|
800
|
+
if (!id) return;
|
|
801
|
+
ed.select(id);
|
|
802
|
+
requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
|
|
803
|
+
};
|
|
804
|
+
useEffect(() => {
|
|
805
|
+
if (!editing) return;
|
|
806
|
+
const onKey = (e) => {
|
|
807
|
+
const t = e.target;
|
|
808
|
+
const typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
|
|
809
|
+
if (e.key === "Escape") return void ed.select(null);
|
|
810
|
+
if (typing) return;
|
|
811
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selection) {
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
removeSelected();
|
|
814
|
+
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d" && selection) {
|
|
815
|
+
e.preventDefault();
|
|
816
|
+
duplicate(selection);
|
|
817
|
+
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c" && selection) {
|
|
818
|
+
copy(selection);
|
|
819
|
+
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
|
|
820
|
+
e.preventDefault();
|
|
821
|
+
paste(selection);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
window.addEventListener("keydown", onKey);
|
|
825
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
826
|
+
}, [editing, selection]);
|
|
827
|
+
const onContextMenuSelect = (e) => {
|
|
828
|
+
if (!editing) return;
|
|
829
|
+
const el = e.target.closest("[data-cms]");
|
|
830
|
+
if (el) ed.select(el.getAttribute("data-cms"));
|
|
831
|
+
else ed.select(null);
|
|
832
|
+
};
|
|
833
|
+
const page = /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) });
|
|
834
|
+
const pageShell = pinned ? /* @__PURE__ */ jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : page;
|
|
681
835
|
return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
|
|
682
|
-
|
|
836
|
+
editing ? /* @__PURE__ */ jsxs(ContextMenu, { children: [
|
|
837
|
+
/* @__PURE__ */ jsx(ContextMenu.Trigger, { children: pageShell }),
|
|
838
|
+
/* @__PURE__ */ jsx(ContextMenu.Content, { className: "!z-[2147483646]", children: /* @__PURE__ */ jsx(
|
|
839
|
+
EditMenuItems,
|
|
840
|
+
{
|
|
841
|
+
node: selNode ?? null,
|
|
842
|
+
hasClipboard: Boolean(clip.current),
|
|
843
|
+
isContainer: selNode ? CONTAINER_TYPES2.has(selNode.type) : false,
|
|
844
|
+
onEditText: () => startEditText(selection),
|
|
845
|
+
onAdd: addNode,
|
|
846
|
+
onDuplicate: () => duplicate(selection),
|
|
847
|
+
onCopy: () => copy(selection),
|
|
848
|
+
onPaste: () => paste(selection),
|
|
849
|
+
onWrap: () => wrapInBox(selection),
|
|
850
|
+
onReorder: (d) => reorder(selection, d),
|
|
851
|
+
onDelete: removeSelected
|
|
852
|
+
}
|
|
853
|
+
) })
|
|
854
|
+
] }) : pageShell,
|
|
683
855
|
editing && box && selection ? /* @__PURE__ */ jsx(
|
|
684
856
|
SelectionOverlay,
|
|
685
857
|
{
|
|
@@ -933,6 +1105,45 @@ function EditBar({
|
|
|
933
1105
|
] }) : null
|
|
934
1106
|
] });
|
|
935
1107
|
}
|
|
1108
|
+
function EditMenuItems({
|
|
1109
|
+
node,
|
|
1110
|
+
hasClipboard,
|
|
1111
|
+
isContainer,
|
|
1112
|
+
onEditText,
|
|
1113
|
+
onAdd,
|
|
1114
|
+
onDuplicate,
|
|
1115
|
+
onCopy,
|
|
1116
|
+
onPaste,
|
|
1117
|
+
onWrap,
|
|
1118
|
+
onReorder,
|
|
1119
|
+
onDelete
|
|
1120
|
+
}) {
|
|
1121
|
+
const editable = node && (node.type === "text" || node.type === "heading");
|
|
1122
|
+
const nested = Boolean(node && node.parent !== null);
|
|
1123
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1124
|
+
editable ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
|
|
1125
|
+
/* @__PURE__ */ jsxs(ContextMenu.Sub, { children: [
|
|
1126
|
+
/* @__PURE__ */ jsx(ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
|
|
1127
|
+
/* @__PURE__ */ jsx(ContextMenu.SubContent, { className: "!z-[2147483647]", children: ADD_MENU.map((m) => /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onAdd(m.kind), children: m.label }, m.kind)) })
|
|
1128
|
+
] }),
|
|
1129
|
+
node ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1130
|
+
/* @__PURE__ */ jsx(ContextMenu.Separator, {}),
|
|
1131
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
|
|
1132
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
|
|
1133
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" }),
|
|
1134
|
+
nested ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onWrap, children: "Wrap in Box" }) : null,
|
|
1135
|
+
/* @__PURE__ */ jsx(ContextMenu.Separator, {}),
|
|
1136
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
|
|
1137
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
|
|
1138
|
+
nested ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1139
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
|
|
1140
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
|
|
1141
|
+
] }) : null,
|
|
1142
|
+
/* @__PURE__ */ jsx(ContextMenu.Separator, {}),
|
|
1143
|
+
/* @__PURE__ */ jsx(ContextMenu.Item, { danger: true, onClick: onDelete, children: "Delete" })
|
|
1144
|
+
] }) : /* @__PURE__ */ jsx(ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" })
|
|
1145
|
+
] });
|
|
1146
|
+
}
|
|
936
1147
|
function applyTransform(el, t) {
|
|
937
1148
|
if (t && (t.x != null || t.y != null || t.scale != null || t.rotate != null || t.opacity != null || t.w != null || t.h != null)) {
|
|
938
1149
|
el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1}) rotate(${t.rotate ?? 0}deg)`;
|