@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.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 page = /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) });
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
- 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,
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)`;