@react-email/editor 0.0.0-experimental.6 → 0.0.0-experimental.7
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/index.d.mts +340 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +339 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +726 -1
- package/dist/index.mjs +700 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Extension, Mark, Node, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as ReactEmailComponents from "@react-email/components";
|
|
4
4
|
import { Button as Button$1, CodeBlock, Column, Row, Section as Section$1 } from "@react-email/components";
|
|
5
5
|
import CodeBlock$1 from "@tiptap/extension-code-block";
|
|
@@ -8,6 +8,11 @@ import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
|
8
8
|
import { fromHtml } from "hast-util-from-html";
|
|
9
9
|
import Prism from "prismjs";
|
|
10
10
|
import TipTapPlaceholder from "@tiptap/extension-placeholder";
|
|
11
|
+
import { useCurrentEditor, useEditorState } from "@tiptap/react";
|
|
12
|
+
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code, CodeIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, StrikethroughIcon, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
15
|
+
import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
|
|
11
16
|
|
|
12
17
|
//#region src/core/email-node.ts
|
|
13
18
|
var EmailNode = class EmailNode extends Node {
|
|
@@ -39,6 +44,48 @@ var EmailNode = class EmailNode extends Node {
|
|
|
39
44
|
}
|
|
40
45
|
};
|
|
41
46
|
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/core/event-bus.ts
|
|
49
|
+
const EVENT_PREFIX = "@react-email/editor:";
|
|
50
|
+
var EditorEventBus = class {
|
|
51
|
+
prefixEventName(eventName) {
|
|
52
|
+
return `${EVENT_PREFIX}${String(eventName)}`;
|
|
53
|
+
}
|
|
54
|
+
dispatch(eventName, payload, options) {
|
|
55
|
+
const target = options?.target ?? window;
|
|
56
|
+
const prefixedEventName = this.prefixEventName(eventName);
|
|
57
|
+
const event = new CustomEvent(prefixedEventName, {
|
|
58
|
+
detail: payload,
|
|
59
|
+
bubbles: false,
|
|
60
|
+
cancelable: false
|
|
61
|
+
});
|
|
62
|
+
target.dispatchEvent(event);
|
|
63
|
+
}
|
|
64
|
+
on(eventName, handler, options) {
|
|
65
|
+
const target = options?.target ?? window;
|
|
66
|
+
const prefixedEventName = this.prefixEventName(eventName);
|
|
67
|
+
const abortController = new AbortController();
|
|
68
|
+
const wrappedHandler = (event) => {
|
|
69
|
+
const customEvent = event;
|
|
70
|
+
const result = handler(customEvent.detail);
|
|
71
|
+
if (result instanceof Promise) result.catch((error) => {
|
|
72
|
+
console.error(`Error in async event handler for ${prefixedEventName}:`, {
|
|
73
|
+
event: customEvent.detail,
|
|
74
|
+
error
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
target.addEventListener(prefixedEventName, wrappedHandler, {
|
|
79
|
+
...options,
|
|
80
|
+
signal: abortController.signal
|
|
81
|
+
});
|
|
82
|
+
return { unsubscribe: () => {
|
|
83
|
+
abortController.abort();
|
|
84
|
+
} };
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const editorEventBus = new EditorEventBus();
|
|
88
|
+
|
|
42
89
|
//#endregion
|
|
43
90
|
//#region src/extensions/alignment-attribute.tsx
|
|
44
91
|
const AlignmentAttribute = Extension.create({
|
|
@@ -772,6 +819,25 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
|
|
|
772
819
|
]
|
|
773
820
|
];
|
|
774
821
|
},
|
|
822
|
+
addKeyboardShortcuts() {
|
|
823
|
+
return {
|
|
824
|
+
...this.parent?.(),
|
|
825
|
+
"Mod-a": ({ editor }) => {
|
|
826
|
+
const { state } = editor;
|
|
827
|
+
const { selection } = state;
|
|
828
|
+
const { $from } = selection;
|
|
829
|
+
for (let depth = $from.depth; depth >= 1; depth--) if ($from.node(depth).type.name === this.name) {
|
|
830
|
+
const blockStart = $from.start(depth);
|
|
831
|
+
const blockEnd = $from.end(depth);
|
|
832
|
+
if (selection.from === blockStart && selection.to === blockEnd) return false;
|
|
833
|
+
const tr = state.tr.setSelection(TextSelection.create(state.doc, blockStart, blockEnd));
|
|
834
|
+
editor.view.dispatch(tr);
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
},
|
|
775
841
|
addProseMirrorPlugins() {
|
|
776
842
|
return [...this.parent?.() || [], PrismPlugin({
|
|
777
843
|
name: this.name,
|
|
@@ -1553,5 +1619,637 @@ const TableHeader = Node.create({
|
|
|
1553
1619
|
});
|
|
1554
1620
|
|
|
1555
1621
|
//#endregion
|
|
1556
|
-
|
|
1622
|
+
//#region src/extensions/uppercase.ts
|
|
1623
|
+
const Uppercase = Mark.create({
|
|
1624
|
+
name: "uppercase",
|
|
1625
|
+
addOptions() {
|
|
1626
|
+
return { HTMLAttributes: {} };
|
|
1627
|
+
},
|
|
1628
|
+
parseHTML() {
|
|
1629
|
+
return [{
|
|
1630
|
+
tag: "span",
|
|
1631
|
+
getAttrs: (node) => {
|
|
1632
|
+
if (node.style.textTransform === "uppercase") return {};
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
}];
|
|
1636
|
+
},
|
|
1637
|
+
renderHTML({ HTMLAttributes }) {
|
|
1638
|
+
return [
|
|
1639
|
+
"span",
|
|
1640
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style: "text-transform: uppercase" }),
|
|
1641
|
+
0
|
|
1642
|
+
];
|
|
1643
|
+
},
|
|
1644
|
+
addCommands() {
|
|
1645
|
+
return {
|
|
1646
|
+
setUppercase: () => ({ commands }) => {
|
|
1647
|
+
return commands.setMark(this.name);
|
|
1648
|
+
},
|
|
1649
|
+
toggleUppercase: () => ({ commands }) => {
|
|
1650
|
+
return commands.toggleMark(this.name);
|
|
1651
|
+
},
|
|
1652
|
+
unsetUppercase: () => ({ commands }) => {
|
|
1653
|
+
return commands.unsetMark(this.name);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
//#endregion
|
|
1660
|
+
//#region src/utils/set-text-alignment.ts
|
|
1661
|
+
function setTextAlignment(editor, alignment) {
|
|
1662
|
+
const { from, to } = editor.state.selection;
|
|
1663
|
+
const tr = editor.state.tr;
|
|
1664
|
+
editor.state.doc.nodesBetween(from, to, (node, pos) => {
|
|
1665
|
+
if (node.isTextblock) {
|
|
1666
|
+
const prop = "align" in node.attrs ? "align" : "alignment";
|
|
1667
|
+
tr.setNodeMarkup(pos, null, {
|
|
1668
|
+
...node.attrs,
|
|
1669
|
+
[prop]: alignment
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
editor.view.dispatch(tr);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
//#endregion
|
|
1677
|
+
//#region src/ui/bubble-menu/context.tsx
|
|
1678
|
+
const BubbleMenuContext = React.createContext(null);
|
|
1679
|
+
function useBubbleMenuContext() {
|
|
1680
|
+
const context = React.useContext(BubbleMenuContext);
|
|
1681
|
+
if (!context) throw new Error("BubbleMenu compound components must be used within <BubbleMenu.Root>");
|
|
1682
|
+
return context;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
//#endregion
|
|
1686
|
+
//#region src/ui/bubble-menu/item.tsx
|
|
1687
|
+
function BubbleMenuItem({ name, isActive, onCommand, className, children,...rest }) {
|
|
1688
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1689
|
+
type: "button",
|
|
1690
|
+
"aria-label": name,
|
|
1691
|
+
"aria-pressed": isActive,
|
|
1692
|
+
className,
|
|
1693
|
+
"data-re-bubble-menu-item": "",
|
|
1694
|
+
"data-item": name,
|
|
1695
|
+
...isActive ? { "data-active": "" } : {},
|
|
1696
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1697
|
+
onClick: onCommand,
|
|
1698
|
+
...rest,
|
|
1699
|
+
children
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
//#endregion
|
|
1704
|
+
//#region src/ui/bubble-menu/align-center.tsx
|
|
1705
|
+
function BubbleMenuAlignCenter({ className, children }) {
|
|
1706
|
+
const { editor } = useBubbleMenuContext();
|
|
1707
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1708
|
+
name: "align-center",
|
|
1709
|
+
isActive: useEditorState({
|
|
1710
|
+
editor,
|
|
1711
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "center" }) ?? false
|
|
1712
|
+
}),
|
|
1713
|
+
onCommand: () => setTextAlignment(editor, "center"),
|
|
1714
|
+
className,
|
|
1715
|
+
children: children ?? /* @__PURE__ */ jsx(AlignCenterIcon, {})
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/ui/bubble-menu/align-left.tsx
|
|
1721
|
+
function BubbleMenuAlignLeft({ className, children }) {
|
|
1722
|
+
const { editor } = useBubbleMenuContext();
|
|
1723
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1724
|
+
name: "align-left",
|
|
1725
|
+
isActive: useEditorState({
|
|
1726
|
+
editor,
|
|
1727
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "left" }) ?? false
|
|
1728
|
+
}),
|
|
1729
|
+
onCommand: () => setTextAlignment(editor, "left"),
|
|
1730
|
+
className,
|
|
1731
|
+
children: children ?? /* @__PURE__ */ jsx(AlignLeftIcon, {})
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
//#endregion
|
|
1736
|
+
//#region src/ui/bubble-menu/align-right.tsx
|
|
1737
|
+
function BubbleMenuAlignRight({ className, children }) {
|
|
1738
|
+
const { editor } = useBubbleMenuContext();
|
|
1739
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1740
|
+
name: "align-right",
|
|
1741
|
+
isActive: useEditorState({
|
|
1742
|
+
editor,
|
|
1743
|
+
selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "right" }) ?? false
|
|
1744
|
+
}),
|
|
1745
|
+
onCommand: () => setTextAlignment(editor, "right"),
|
|
1746
|
+
className,
|
|
1747
|
+
children: children ?? /* @__PURE__ */ jsx(AlignRightIcon, {})
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
//#endregion
|
|
1752
|
+
//#region src/ui/bubble-menu/create-mark-bubble-item.tsx
|
|
1753
|
+
function createMarkBubbleItem(config) {
|
|
1754
|
+
function MarkBubbleItem({ className, children }) {
|
|
1755
|
+
const { editor } = useBubbleMenuContext();
|
|
1756
|
+
const isActive = useEditorState({
|
|
1757
|
+
editor,
|
|
1758
|
+
selector: ({ editor: editor$1 }) => {
|
|
1759
|
+
if (config.activeParams) return editor$1?.isActive(config.activeName, config.activeParams) ?? false;
|
|
1760
|
+
return editor$1?.isActive(config.activeName) ?? false;
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
const handleCommand = () => {
|
|
1764
|
+
const chain = editor.chain().focus();
|
|
1765
|
+
const method = chain[config.command];
|
|
1766
|
+
if (method) method.call(chain).run();
|
|
1767
|
+
};
|
|
1768
|
+
return /* @__PURE__ */ jsx(BubbleMenuItem, {
|
|
1769
|
+
name: config.name,
|
|
1770
|
+
isActive,
|
|
1771
|
+
onCommand: handleCommand,
|
|
1772
|
+
className,
|
|
1773
|
+
children: children ?? config.icon
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
MarkBubbleItem.displayName = `BubbleMenu${config.name.charAt(0).toUpperCase() + config.name.slice(1)}`;
|
|
1777
|
+
return MarkBubbleItem;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region src/ui/bubble-menu/bold.tsx
|
|
1782
|
+
const BubbleMenuBold = createMarkBubbleItem({
|
|
1783
|
+
name: "bold",
|
|
1784
|
+
activeName: "bold",
|
|
1785
|
+
command: "toggleBold",
|
|
1786
|
+
icon: /* @__PURE__ */ jsx(BoldIcon, {})
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region src/ui/bubble-menu/code.tsx
|
|
1791
|
+
const BubbleMenuCode = createMarkBubbleItem({
|
|
1792
|
+
name: "code",
|
|
1793
|
+
activeName: "code",
|
|
1794
|
+
command: "toggleCode",
|
|
1795
|
+
icon: /* @__PURE__ */ jsx(CodeIcon, {})
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
//#endregion
|
|
1799
|
+
//#region src/ui/bubble-menu/group.tsx
|
|
1800
|
+
function BubbleMenuItemGroup({ className, children }) {
|
|
1801
|
+
return /* @__PURE__ */ jsx("fieldset", {
|
|
1802
|
+
className,
|
|
1803
|
+
"data-re-bubble-menu-group": "",
|
|
1804
|
+
children
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
//#endregion
|
|
1809
|
+
//#region src/ui/bubble-menu/italic.tsx
|
|
1810
|
+
const BubbleMenuItalic = createMarkBubbleItem({
|
|
1811
|
+
name: "italic",
|
|
1812
|
+
activeName: "italic",
|
|
1813
|
+
command: "toggleItalic",
|
|
1814
|
+
icon: /* @__PURE__ */ jsx(ItalicIcon, {})
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
//#endregion
|
|
1818
|
+
//#region src/ui/bubble-menu/utils.ts
|
|
1819
|
+
const SAFE_PROTOCOLS = new Set([
|
|
1820
|
+
"http:",
|
|
1821
|
+
"https:",
|
|
1822
|
+
"mailto:",
|
|
1823
|
+
"tel:"
|
|
1824
|
+
]);
|
|
1825
|
+
/**
|
|
1826
|
+
* Basic URL validation and auto-prefixing.
|
|
1827
|
+
* Rejects dangerous schemes (javascript:, data:, vbscript:, etc.).
|
|
1828
|
+
* Returns the valid URL string or null.
|
|
1829
|
+
*/
|
|
1830
|
+
function getUrlFromString(str) {
|
|
1831
|
+
if (str === "#") return str;
|
|
1832
|
+
try {
|
|
1833
|
+
const url = new URL(str);
|
|
1834
|
+
if (SAFE_PROTOCOLS.has(url.protocol)) return str;
|
|
1835
|
+
return null;
|
|
1836
|
+
} catch {}
|
|
1837
|
+
try {
|
|
1838
|
+
if (str.includes(".") && !str.includes(" ")) return new URL(`https://${str}`).toString();
|
|
1839
|
+
} catch {}
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
//#endregion
|
|
1844
|
+
//#region src/ui/bubble-menu/link-selector.tsx
|
|
1845
|
+
function BubbleMenuLinkSelector({ className, showToggle = true, validateUrl, onLinkApply, onLinkRemove, children }) {
|
|
1846
|
+
const { editor } = useBubbleMenuContext();
|
|
1847
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
1848
|
+
const editorState = useEditorState({
|
|
1849
|
+
editor,
|
|
1850
|
+
selector: ({ editor: editor$1 }) => ({
|
|
1851
|
+
isLinkActive: editor$1?.isActive("link") ?? false,
|
|
1852
|
+
hasLink: Boolean(editor$1?.getAttributes("link").href),
|
|
1853
|
+
currentHref: editor$1?.getAttributes("link").href || ""
|
|
1854
|
+
})
|
|
1855
|
+
});
|
|
1856
|
+
React.useEffect(() => {
|
|
1857
|
+
const subscription = editorEventBus.on("bubble-menu:add-link", () => {
|
|
1858
|
+
setIsOpen(true);
|
|
1859
|
+
});
|
|
1860
|
+
return () => {
|
|
1861
|
+
setIsOpen(false);
|
|
1862
|
+
subscription.unsubscribe();
|
|
1863
|
+
};
|
|
1864
|
+
}, []);
|
|
1865
|
+
if (!editorState) return null;
|
|
1866
|
+
const handleOpenLink = () => {
|
|
1867
|
+
setIsOpen(!isOpen);
|
|
1868
|
+
};
|
|
1869
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1870
|
+
"data-re-link-selector": "",
|
|
1871
|
+
...isOpen ? { "data-open": "" } : {},
|
|
1872
|
+
...editorState.hasLink ? { "data-has-link": "" } : {},
|
|
1873
|
+
className,
|
|
1874
|
+
children: [showToggle && /* @__PURE__ */ jsx("button", {
|
|
1875
|
+
type: "button",
|
|
1876
|
+
"aria-expanded": isOpen,
|
|
1877
|
+
"aria-haspopup": "true",
|
|
1878
|
+
"aria-label": "Add link",
|
|
1879
|
+
"aria-pressed": editorState.isLinkActive && editorState.hasLink,
|
|
1880
|
+
"data-re-link-selector-trigger": "",
|
|
1881
|
+
onClick: handleOpenLink,
|
|
1882
|
+
children: /* @__PURE__ */ jsx(LinkIcon, {})
|
|
1883
|
+
}), isOpen && /* @__PURE__ */ jsx(LinkForm, {
|
|
1884
|
+
editor,
|
|
1885
|
+
currentHref: editorState.currentHref,
|
|
1886
|
+
validateUrl,
|
|
1887
|
+
onLinkApply,
|
|
1888
|
+
onLinkRemove,
|
|
1889
|
+
setIsOpen,
|
|
1890
|
+
children
|
|
1891
|
+
})]
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
function LinkForm({ editor, currentHref, validateUrl, onLinkApply, onLinkRemove, setIsOpen, children }) {
|
|
1895
|
+
const inputRef = React.useRef(null);
|
|
1896
|
+
const formRef = React.useRef(null);
|
|
1897
|
+
const displayHref = currentHref === "#" ? "" : currentHref;
|
|
1898
|
+
const [inputValue, setInputValue] = React.useState(displayHref);
|
|
1899
|
+
React.useEffect(() => {
|
|
1900
|
+
const timeoutId = setTimeout(() => {
|
|
1901
|
+
inputRef.current?.focus();
|
|
1902
|
+
}, 0);
|
|
1903
|
+
return () => clearTimeout(timeoutId);
|
|
1904
|
+
}, []);
|
|
1905
|
+
React.useEffect(() => {
|
|
1906
|
+
const handleKeyDown = (event) => {
|
|
1907
|
+
if (event.key === "Escape") {
|
|
1908
|
+
if (editor.getAttributes("link").href === "#") editor.chain().unsetLink().run();
|
|
1909
|
+
setIsOpen(false);
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
const handleClickOutside = (event) => {
|
|
1913
|
+
if (formRef.current && !formRef.current.contains(event.target)) {
|
|
1914
|
+
const form = formRef.current;
|
|
1915
|
+
const submitEvent = new Event("submit", {
|
|
1916
|
+
bubbles: true,
|
|
1917
|
+
cancelable: true
|
|
1918
|
+
});
|
|
1919
|
+
form.dispatchEvent(submitEvent);
|
|
1920
|
+
setIsOpen(false);
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
1924
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1925
|
+
return () => {
|
|
1926
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
1927
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
1928
|
+
};
|
|
1929
|
+
}, [editor, setIsOpen]);
|
|
1930
|
+
function handleSubmit(e) {
|
|
1931
|
+
e.preventDefault();
|
|
1932
|
+
const value = inputValue.trim();
|
|
1933
|
+
if (value === "") {
|
|
1934
|
+
setLinkHref(editor, "");
|
|
1935
|
+
setIsOpen(false);
|
|
1936
|
+
focusEditor(editor);
|
|
1937
|
+
onLinkRemove?.();
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const finalValue = (validateUrl ?? getUrlFromString)(value);
|
|
1941
|
+
if (!finalValue) {
|
|
1942
|
+
setLinkHref(editor, "");
|
|
1943
|
+
setIsOpen(false);
|
|
1944
|
+
focusEditor(editor);
|
|
1945
|
+
onLinkRemove?.();
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
setLinkHref(editor, finalValue);
|
|
1949
|
+
setIsOpen(false);
|
|
1950
|
+
focusEditor(editor);
|
|
1951
|
+
onLinkApply?.(finalValue);
|
|
1952
|
+
}
|
|
1953
|
+
function handleUnlink(e) {
|
|
1954
|
+
e.stopPropagation();
|
|
1955
|
+
setLinkHref(editor, "");
|
|
1956
|
+
setIsOpen(false);
|
|
1957
|
+
focusEditor(editor);
|
|
1958
|
+
onLinkRemove?.();
|
|
1959
|
+
}
|
|
1960
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
1961
|
+
ref: formRef,
|
|
1962
|
+
"data-re-link-selector-form": "",
|
|
1963
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1964
|
+
onClick: (e) => e.stopPropagation(),
|
|
1965
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
1966
|
+
onSubmit: handleSubmit,
|
|
1967
|
+
children: [
|
|
1968
|
+
/* @__PURE__ */ jsx("input", {
|
|
1969
|
+
ref: inputRef,
|
|
1970
|
+
"data-re-link-selector-input": "",
|
|
1971
|
+
value: inputValue,
|
|
1972
|
+
onFocus: (e) => e.stopPropagation(),
|
|
1973
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
1974
|
+
placeholder: "Paste a link",
|
|
1975
|
+
type: "text"
|
|
1976
|
+
}),
|
|
1977
|
+
children,
|
|
1978
|
+
displayHref ? /* @__PURE__ */ jsx("button", {
|
|
1979
|
+
type: "button",
|
|
1980
|
+
"aria-label": "Remove link",
|
|
1981
|
+
"data-re-link-selector-unlink": "",
|
|
1982
|
+
onClick: handleUnlink,
|
|
1983
|
+
children: /* @__PURE__ */ jsx(UnlinkIcon, {})
|
|
1984
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
1985
|
+
type: "submit",
|
|
1986
|
+
"aria-label": "Apply link",
|
|
1987
|
+
"data-re-link-selector-apply": "",
|
|
1988
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1989
|
+
children: /* @__PURE__ */ jsx(Check, {})
|
|
1990
|
+
})
|
|
1991
|
+
]
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
function setLinkHref(editor, href) {
|
|
1995
|
+
if (href.length === 0) {
|
|
1996
|
+
editor.chain().unsetLink().run();
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const { from, to } = editor.state.selection;
|
|
2000
|
+
if (from === to) {
|
|
2001
|
+
editor.chain().extendMarkRange("link").setLink({ href }).setTextSelection({
|
|
2002
|
+
from,
|
|
2003
|
+
to
|
|
2004
|
+
}).run();
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
editor.chain().setLink({ href }).run();
|
|
2008
|
+
}
|
|
2009
|
+
function focusEditor(editor) {
|
|
2010
|
+
setTimeout(() => {
|
|
2011
|
+
editor.commands.focus();
|
|
2012
|
+
}, 0);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
//#endregion
|
|
2016
|
+
//#region src/ui/bubble-menu/node-selector.tsx
|
|
2017
|
+
const NodeSelectorContext = React.createContext(null);
|
|
2018
|
+
function useNodeSelectorContext() {
|
|
2019
|
+
const context = React.useContext(NodeSelectorContext);
|
|
2020
|
+
if (!context) throw new Error("NodeSelector compound components must be used within <NodeSelector.Root>");
|
|
2021
|
+
return context;
|
|
2022
|
+
}
|
|
2023
|
+
function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, className, children }) {
|
|
2024
|
+
const { editor } = useBubbleMenuContext();
|
|
2025
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
|
|
2026
|
+
const isControlled = controlledOpen !== void 0;
|
|
2027
|
+
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
|
2028
|
+
const setIsOpen = React.useCallback((value) => {
|
|
2029
|
+
if (!isControlled) setUncontrolledOpen(value);
|
|
2030
|
+
onOpenChange?.(value);
|
|
2031
|
+
}, [isControlled, onOpenChange]);
|
|
2032
|
+
const editorState = useEditorState({
|
|
2033
|
+
editor,
|
|
2034
|
+
selector: ({ editor: editor$1 }) => ({
|
|
2035
|
+
isParagraphActive: (editor$1?.isActive("paragraph") ?? false) && !editor$1?.isActive("bulletList") && !editor$1?.isActive("orderedList"),
|
|
2036
|
+
isHeading1Active: editor$1?.isActive("heading", { level: 1 }) ?? false,
|
|
2037
|
+
isHeading2Active: editor$1?.isActive("heading", { level: 2 }) ?? false,
|
|
2038
|
+
isHeading3Active: editor$1?.isActive("heading", { level: 3 }) ?? false,
|
|
2039
|
+
isBulletListActive: editor$1?.isActive("bulletList") ?? false,
|
|
2040
|
+
isOrderedListActive: editor$1?.isActive("orderedList") ?? false,
|
|
2041
|
+
isBlockquoteActive: editor$1?.isActive("blockquote") ?? false,
|
|
2042
|
+
isCodeBlockActive: editor$1?.isActive("codeBlock") ?? false
|
|
2043
|
+
})
|
|
2044
|
+
});
|
|
2045
|
+
const allItems = React.useMemo(() => [
|
|
2046
|
+
{
|
|
2047
|
+
name: "Text",
|
|
2048
|
+
icon: TextIcon,
|
|
2049
|
+
command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").run(),
|
|
2050
|
+
isActive: editorState?.isParagraphActive ?? false
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
name: "Title",
|
|
2054
|
+
icon: Heading1,
|
|
2055
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),
|
|
2056
|
+
isActive: editorState?.isHeading1Active ?? false
|
|
2057
|
+
},
|
|
2058
|
+
{
|
|
2059
|
+
name: "Subtitle",
|
|
2060
|
+
icon: Heading2,
|
|
2061
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),
|
|
2062
|
+
isActive: editorState?.isHeading2Active ?? false
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
name: "Heading",
|
|
2066
|
+
icon: Heading3,
|
|
2067
|
+
command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),
|
|
2068
|
+
isActive: editorState?.isHeading3Active ?? false
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
name: "Bullet List",
|
|
2072
|
+
icon: List,
|
|
2073
|
+
command: () => editor.chain().focus().clearNodes().toggleBulletList().run(),
|
|
2074
|
+
isActive: editorState?.isBulletListActive ?? false
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
name: "Numbered List",
|
|
2078
|
+
icon: ListOrdered,
|
|
2079
|
+
command: () => editor.chain().focus().clearNodes().toggleOrderedList().run(),
|
|
2080
|
+
isActive: editorState?.isOrderedListActive ?? false
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
name: "Quote",
|
|
2084
|
+
icon: TextQuote,
|
|
2085
|
+
command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
|
2086
|
+
isActive: editorState?.isBlockquoteActive ?? false
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
name: "Code",
|
|
2090
|
+
icon: Code,
|
|
2091
|
+
command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
|
|
2092
|
+
isActive: editorState?.isCodeBlockActive ?? false
|
|
2093
|
+
}
|
|
2094
|
+
], [editor, editorState]);
|
|
2095
|
+
const items = React.useMemo(() => allItems.filter((item) => !omit.includes(item.name)), [allItems, omit]);
|
|
2096
|
+
const activeItem = React.useMemo(() => items.find((item) => item.isActive) ?? { name: "Multiple" }, [items]);
|
|
2097
|
+
const contextValue = React.useMemo(() => ({
|
|
2098
|
+
items,
|
|
2099
|
+
activeItem,
|
|
2100
|
+
isOpen,
|
|
2101
|
+
setIsOpen
|
|
2102
|
+
}), [
|
|
2103
|
+
items,
|
|
2104
|
+
activeItem,
|
|
2105
|
+
isOpen,
|
|
2106
|
+
setIsOpen
|
|
2107
|
+
]);
|
|
2108
|
+
if (!editorState || items.length === 0) return null;
|
|
2109
|
+
return /* @__PURE__ */ jsx(NodeSelectorContext.Provider, {
|
|
2110
|
+
value: contextValue,
|
|
2111
|
+
children: /* @__PURE__ */ jsx(Popover.Root, {
|
|
2112
|
+
open: isOpen,
|
|
2113
|
+
onOpenChange: setIsOpen,
|
|
2114
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2115
|
+
"data-re-node-selector": "",
|
|
2116
|
+
...isOpen ? { "data-open": "" } : {},
|
|
2117
|
+
className,
|
|
2118
|
+
children
|
|
2119
|
+
})
|
|
2120
|
+
})
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
function NodeSelectorTrigger({ className, children }) {
|
|
2124
|
+
const { activeItem, isOpen, setIsOpen } = useNodeSelectorContext();
|
|
2125
|
+
return /* @__PURE__ */ jsx(Popover.Trigger, {
|
|
2126
|
+
"data-re-node-selector-trigger": "",
|
|
2127
|
+
className,
|
|
2128
|
+
onClick: () => setIsOpen(!isOpen),
|
|
2129
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: activeItem.name }), /* @__PURE__ */ jsx(ChevronDown, {})] })
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
function NodeSelectorContent({ className, align = "start", children }) {
|
|
2133
|
+
const { items, setIsOpen } = useNodeSelectorContext();
|
|
2134
|
+
return /* @__PURE__ */ jsx(Popover.Content, {
|
|
2135
|
+
align,
|
|
2136
|
+
"data-re-node-selector-content": "",
|
|
2137
|
+
className,
|
|
2138
|
+
children: children ? children(items, () => setIsOpen(false)) : items.map((item) => {
|
|
2139
|
+
const Icon = item.icon;
|
|
2140
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
2141
|
+
type: "button",
|
|
2142
|
+
"data-re-node-selector-item": "",
|
|
2143
|
+
...item.isActive ? { "data-active": "" } : {},
|
|
2144
|
+
onClick: () => {
|
|
2145
|
+
item.command();
|
|
2146
|
+
setIsOpen(false);
|
|
2147
|
+
},
|
|
2148
|
+
children: [
|
|
2149
|
+
/* @__PURE__ */ jsx(Icon, {}),
|
|
2150
|
+
/* @__PURE__ */ jsx("span", { children: item.name }),
|
|
2151
|
+
item.isActive && /* @__PURE__ */ jsx(Check, {})
|
|
2152
|
+
]
|
|
2153
|
+
}, item.name);
|
|
2154
|
+
})
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
function BubbleMenuNodeSelector({ omit = [], className, triggerContent, open, onOpenChange }) {
|
|
2158
|
+
return /* @__PURE__ */ jsxs(NodeSelectorRoot, {
|
|
2159
|
+
omit,
|
|
2160
|
+
open,
|
|
2161
|
+
onOpenChange,
|
|
2162
|
+
className,
|
|
2163
|
+
children: [/* @__PURE__ */ jsx(NodeSelectorTrigger, { children: triggerContent }), /* @__PURE__ */ jsx(NodeSelectorContent, {})]
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
//#endregion
|
|
2168
|
+
//#region src/ui/bubble-menu/root.tsx
|
|
2169
|
+
function BubbleMenuRoot({ excludeNodes = [], placement = "bottom", offset = 8, onHide, className, children }) {
|
|
2170
|
+
const { editor } = useCurrentEditor();
|
|
2171
|
+
if (!editor) return null;
|
|
2172
|
+
return /* @__PURE__ */ jsx(BubbleMenu$1, {
|
|
2173
|
+
editor,
|
|
2174
|
+
"data-re-bubble-menu": "",
|
|
2175
|
+
shouldShow: ({ editor: editor$1, view }) => {
|
|
2176
|
+
for (const node of excludeNodes) if (editor$1.isActive(node)) return false;
|
|
2177
|
+
if (view.dom.classList.contains("dragging")) return false;
|
|
2178
|
+
return editor$1.view.state.selection.content().size > 0;
|
|
2179
|
+
},
|
|
2180
|
+
options: {
|
|
2181
|
+
placement,
|
|
2182
|
+
offset,
|
|
2183
|
+
onHide
|
|
2184
|
+
},
|
|
2185
|
+
className,
|
|
2186
|
+
children: /* @__PURE__ */ jsx(BubbleMenuContext.Provider, {
|
|
2187
|
+
value: { editor },
|
|
2188
|
+
children
|
|
2189
|
+
})
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
//#endregion
|
|
2194
|
+
//#region src/ui/bubble-menu/separator.tsx
|
|
2195
|
+
function BubbleMenuSeparator({ className }) {
|
|
2196
|
+
return /* @__PURE__ */ jsx("hr", {
|
|
2197
|
+
className,
|
|
2198
|
+
"data-re-bubble-menu-separator": ""
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
//#endregion
|
|
2203
|
+
//#region src/ui/bubble-menu/strike.tsx
|
|
2204
|
+
const BubbleMenuStrike = createMarkBubbleItem({
|
|
2205
|
+
name: "strike",
|
|
2206
|
+
activeName: "strike",
|
|
2207
|
+
command: "toggleStrike",
|
|
2208
|
+
icon: /* @__PURE__ */ jsx(StrikethroughIcon, {})
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
//#endregion
|
|
2212
|
+
//#region src/ui/bubble-menu/underline.tsx
|
|
2213
|
+
const BubbleMenuUnderline = createMarkBubbleItem({
|
|
2214
|
+
name: "underline",
|
|
2215
|
+
activeName: "underline",
|
|
2216
|
+
command: "toggleUnderline",
|
|
2217
|
+
icon: /* @__PURE__ */ jsx(UnderlineIcon, {})
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
//#endregion
|
|
2221
|
+
//#region src/ui/bubble-menu/uppercase.tsx
|
|
2222
|
+
const BubbleMenuUppercase = createMarkBubbleItem({
|
|
2223
|
+
name: "uppercase",
|
|
2224
|
+
activeName: "uppercase",
|
|
2225
|
+
command: "toggleUppercase",
|
|
2226
|
+
icon: /* @__PURE__ */ jsx(CaseUpperIcon, {})
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
//#endregion
|
|
2230
|
+
//#region src/ui/bubble-menu/index.ts
|
|
2231
|
+
const BubbleMenu = {
|
|
2232
|
+
Root: BubbleMenuRoot,
|
|
2233
|
+
ItemGroup: BubbleMenuItemGroup,
|
|
2234
|
+
Separator: BubbleMenuSeparator,
|
|
2235
|
+
Item: BubbleMenuItem,
|
|
2236
|
+
Bold: BubbleMenuBold,
|
|
2237
|
+
Italic: BubbleMenuItalic,
|
|
2238
|
+
Underline: BubbleMenuUnderline,
|
|
2239
|
+
Strike: BubbleMenuStrike,
|
|
2240
|
+
Code: BubbleMenuCode,
|
|
2241
|
+
Uppercase: BubbleMenuUppercase,
|
|
2242
|
+
AlignLeft: BubbleMenuAlignLeft,
|
|
2243
|
+
AlignCenter: BubbleMenuAlignCenter,
|
|
2244
|
+
AlignRight: BubbleMenuAlignRight,
|
|
2245
|
+
NodeSelector: Object.assign(BubbleMenuNodeSelector, {
|
|
2246
|
+
Root: NodeSelectorRoot,
|
|
2247
|
+
Trigger: NodeSelectorTrigger,
|
|
2248
|
+
Content: NodeSelectorContent
|
|
2249
|
+
}),
|
|
2250
|
+
LinkSelector: BubbleMenuLinkSelector
|
|
2251
|
+
};
|
|
2252
|
+
|
|
2253
|
+
//#endregion
|
|
2254
|
+
export { AlignmentAttribute, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, Button, COLUMN_PARENT_TYPES, ClassAttribute, CodeBlockPrism, ColumnsColumn, Div, EmailNode, FourColumns, MAX_COLUMNS_DEPTH, MaxNesting, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, Placeholder, PreservedStyle, PreviewText, Section, StyleAttribute, Sup, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, editorEventBus, getColumnsDepth, processStylesForUnlink, setTextAlignment };
|
|
1557
2255
|
//# sourceMappingURL=index.mjs.map
|