@codella-software/react 2.1.0 → 2.2.0
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.cjs +777 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +778 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react/src/index.d.ts +1 -0
- package/dist/react/src/index.d.ts.map +1 -1
- package/dist/react/src/rich-content/index.d.ts +5 -0
- package/dist/react/src/rich-content/index.d.ts.map +1 -0
- package/dist/react/src/rich-content/useRichContent.d.ts +70 -0
- package/dist/react/src/rich-content/useRichContent.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo, createContext, useContext, useCallback } from "react";
|
|
1
|
+
import { useState, useEffect, useMemo, createContext, useContext, useCallback, useRef } from "react";
|
|
2
2
|
import { TableBuilder } from "@codella/core";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { fromEvent, Observable as Observable$1, of, EMPTY, Subject as Subject$1, BehaviorSubject, timer } from "rxjs";
|
|
@@ -1629,6 +1629,782 @@ const useLiveUpdates = (eventName) => {
|
|
|
1629
1629
|
}, [eventName, service]);
|
|
1630
1630
|
return { data, error };
|
|
1631
1631
|
};
|
|
1632
|
+
class ContentModel {
|
|
1633
|
+
constructor(doc) {
|
|
1634
|
+
this.document = doc || this.createEmptyDocument();
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Create empty document with single empty paragraph
|
|
1638
|
+
*/
|
|
1639
|
+
static createEmpty() {
|
|
1640
|
+
return {
|
|
1641
|
+
type: "document",
|
|
1642
|
+
version: "1.0",
|
|
1643
|
+
children: [
|
|
1644
|
+
{
|
|
1645
|
+
type: "paragraph",
|
|
1646
|
+
children: [{ type: "text", text: "" }]
|
|
1647
|
+
}
|
|
1648
|
+
]
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Get the current document
|
|
1653
|
+
*/
|
|
1654
|
+
getDocument() {
|
|
1655
|
+
return JSON.parse(JSON.stringify(this.document));
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Replace entire document
|
|
1659
|
+
*/
|
|
1660
|
+
setDocument(doc) {
|
|
1661
|
+
this.document = JSON.parse(JSON.stringify(doc));
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Get all text content as plain string
|
|
1665
|
+
*/
|
|
1666
|
+
getPlainText() {
|
|
1667
|
+
let text = "";
|
|
1668
|
+
const traverse = (node) => {
|
|
1669
|
+
if (node.type === "text") {
|
|
1670
|
+
text += node.text;
|
|
1671
|
+
} else if ("children" in node) {
|
|
1672
|
+
node.children.forEach(traverse);
|
|
1673
|
+
} else if (node.type === "image") {
|
|
1674
|
+
text += "[Image]";
|
|
1675
|
+
} else if (node.type === "mention") {
|
|
1676
|
+
text += `@${node.label}`;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
this.document.children.forEach(traverse);
|
|
1680
|
+
return text;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Insert text at selection/cursor position
|
|
1684
|
+
*/
|
|
1685
|
+
insertText(text, marks) {
|
|
1686
|
+
const doc = this.cloneDocument();
|
|
1687
|
+
const firstPara = this.getFirstParagraph(doc);
|
|
1688
|
+
if (firstPara && firstPara.children.length > 0) {
|
|
1689
|
+
const firstChild = firstPara.children[0];
|
|
1690
|
+
if (firstChild.type === "text") {
|
|
1691
|
+
firstChild.text += text;
|
|
1692
|
+
if (marks) {
|
|
1693
|
+
firstChild.marks = marks;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return doc;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Insert a paragraph at the end
|
|
1701
|
+
*/
|
|
1702
|
+
insertParagraph(content) {
|
|
1703
|
+
const doc = this.cloneDocument();
|
|
1704
|
+
doc.children.push({
|
|
1705
|
+
type: "paragraph",
|
|
1706
|
+
children: content || [{ type: "text", text: "" }]
|
|
1707
|
+
});
|
|
1708
|
+
return doc;
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Insert heading
|
|
1712
|
+
*/
|
|
1713
|
+
insertHeading(level, content) {
|
|
1714
|
+
const doc = this.cloneDocument();
|
|
1715
|
+
doc.children.push({
|
|
1716
|
+
type: "heading",
|
|
1717
|
+
level,
|
|
1718
|
+
children: content || [{ type: "text", text: "" }]
|
|
1719
|
+
});
|
|
1720
|
+
return doc;
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Insert image
|
|
1724
|
+
*/
|
|
1725
|
+
insertImage(url, attrs) {
|
|
1726
|
+
const doc = this.cloneDocument();
|
|
1727
|
+
const lastBlock = doc.children[doc.children.length - 1];
|
|
1728
|
+
if ((attrs == null ? void 0 : attrs.display) === "block") {
|
|
1729
|
+
doc.children.push({
|
|
1730
|
+
type: "paragraph",
|
|
1731
|
+
children: [
|
|
1732
|
+
{
|
|
1733
|
+
type: "image",
|
|
1734
|
+
url,
|
|
1735
|
+
attrs: attrs || {}
|
|
1736
|
+
}
|
|
1737
|
+
]
|
|
1738
|
+
});
|
|
1739
|
+
} else {
|
|
1740
|
+
if (lastBlock && lastBlock.type === "paragraph") {
|
|
1741
|
+
lastBlock.children.push({
|
|
1742
|
+
type: "image",
|
|
1743
|
+
url,
|
|
1744
|
+
attrs: attrs || {}
|
|
1745
|
+
});
|
|
1746
|
+
} else {
|
|
1747
|
+
doc.children.push({
|
|
1748
|
+
type: "paragraph",
|
|
1749
|
+
children: [
|
|
1750
|
+
{
|
|
1751
|
+
type: "image",
|
|
1752
|
+
url,
|
|
1753
|
+
attrs: attrs || {}
|
|
1754
|
+
}
|
|
1755
|
+
]
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return doc;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Insert mention
|
|
1763
|
+
*/
|
|
1764
|
+
insertMention(id, label, meta) {
|
|
1765
|
+
const doc = this.cloneDocument();
|
|
1766
|
+
const lastBlock = doc.children[doc.children.length - 1];
|
|
1767
|
+
if (lastBlock && lastBlock.type === "paragraph") {
|
|
1768
|
+
lastBlock.children.push({
|
|
1769
|
+
type: "mention",
|
|
1770
|
+
id,
|
|
1771
|
+
label,
|
|
1772
|
+
meta
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
return doc;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Insert list
|
|
1779
|
+
*/
|
|
1780
|
+
insertList(listType, items = [[]]) {
|
|
1781
|
+
const doc = this.cloneDocument();
|
|
1782
|
+
const children = items.map((item) => ({
|
|
1783
|
+
type: "list-item",
|
|
1784
|
+
children: item || [{ type: "text", text: "" }],
|
|
1785
|
+
depth: 0
|
|
1786
|
+
}));
|
|
1787
|
+
doc.children.push({
|
|
1788
|
+
type: "list",
|
|
1789
|
+
listType,
|
|
1790
|
+
children
|
|
1791
|
+
});
|
|
1792
|
+
return doc;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Toggle a mark type on text in a range
|
|
1796
|
+
*/
|
|
1797
|
+
toggleMark(mark, selection) {
|
|
1798
|
+
var _a;
|
|
1799
|
+
const doc = this.cloneDocument();
|
|
1800
|
+
const firstPara = this.getFirstParagraph(doc);
|
|
1801
|
+
if (firstPara && ((_a = firstPara.children[0]) == null ? void 0 : _a.type) === "text") {
|
|
1802
|
+
const textNode = firstPara.children[0];
|
|
1803
|
+
if (!textNode.marks) {
|
|
1804
|
+
textNode.marks = [];
|
|
1805
|
+
}
|
|
1806
|
+
const existingMark = textNode.marks.findIndex((m) => m.type === mark);
|
|
1807
|
+
if (existingMark >= 0) {
|
|
1808
|
+
textNode.marks.splice(existingMark, 1);
|
|
1809
|
+
} else {
|
|
1810
|
+
textNode.marks.push({ type: mark });
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return doc;
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Check if a mark type is active at cursor
|
|
1817
|
+
*/
|
|
1818
|
+
isMarkActive(mark) {
|
|
1819
|
+
var _a, _b;
|
|
1820
|
+
const firstPara = this.getFirstParagraph(this.document);
|
|
1821
|
+
if (((_a = firstPara == null ? void 0 : firstPara.children[0]) == null ? void 0 : _a.type) === "text") {
|
|
1822
|
+
const textNode = firstPara.children[0];
|
|
1823
|
+
return ((_b = textNode.marks) == null ? void 0 : _b.some((m) => m.type === mark)) || false;
|
|
1824
|
+
}
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Get active marks at cursor
|
|
1829
|
+
*/
|
|
1830
|
+
getActiveMarks() {
|
|
1831
|
+
var _a, _b;
|
|
1832
|
+
const firstPara = this.getFirstParagraph(this.document);
|
|
1833
|
+
if (((_a = firstPara == null ? void 0 : firstPara.children[0]) == null ? void 0 : _a.type) === "text") {
|
|
1834
|
+
const textNode = firstPara.children[0];
|
|
1835
|
+
return ((_b = textNode.marks) == null ? void 0 : _b.map((m) => m.type)) || [];
|
|
1836
|
+
}
|
|
1837
|
+
return [];
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Delete content (simplified - deletes last block)
|
|
1841
|
+
*/
|
|
1842
|
+
deleteContent(selection) {
|
|
1843
|
+
const doc = this.cloneDocument();
|
|
1844
|
+
if (doc.children.length > 1) {
|
|
1845
|
+
doc.children.pop();
|
|
1846
|
+
} else if (doc.children.length === 1 && doc.children[0].type === "paragraph") {
|
|
1847
|
+
doc.children[0].children = [{ type: "text", text: "" }];
|
|
1848
|
+
}
|
|
1849
|
+
return doc;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Replace entire content
|
|
1853
|
+
*/
|
|
1854
|
+
replaceContent(nodes) {
|
|
1855
|
+
const doc = this.cloneDocument();
|
|
1856
|
+
doc.children = nodes;
|
|
1857
|
+
return doc;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Clone document (deep copy)
|
|
1861
|
+
*/
|
|
1862
|
+
cloneDocument() {
|
|
1863
|
+
return JSON.parse(JSON.stringify(this.document));
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Get first paragraph in document
|
|
1867
|
+
*/
|
|
1868
|
+
getFirstParagraph(doc) {
|
|
1869
|
+
for (const block of doc.children) {
|
|
1870
|
+
if (block.type === "paragraph") {
|
|
1871
|
+
return block;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Find node by predicate
|
|
1878
|
+
*/
|
|
1879
|
+
findNode(predicate) {
|
|
1880
|
+
const search = (node) => {
|
|
1881
|
+
if (predicate(node)) {
|
|
1882
|
+
return node;
|
|
1883
|
+
}
|
|
1884
|
+
if ("children" in node) {
|
|
1885
|
+
for (const child of node.children) {
|
|
1886
|
+
const result = search(child);
|
|
1887
|
+
if (result) return result;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return null;
|
|
1891
|
+
};
|
|
1892
|
+
for (const child of this.document.children) {
|
|
1893
|
+
const result = search(child);
|
|
1894
|
+
if (result) return result;
|
|
1895
|
+
}
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Count all nodes of a type
|
|
1900
|
+
*/
|
|
1901
|
+
countNodes(type) {
|
|
1902
|
+
let count = 0;
|
|
1903
|
+
const traverse = (node) => {
|
|
1904
|
+
if (node.type === type) count++;
|
|
1905
|
+
if ("children" in node) {
|
|
1906
|
+
node.children.forEach(traverse);
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
this.document.children.forEach(traverse);
|
|
1910
|
+
return count;
|
|
1911
|
+
}
|
|
1912
|
+
createEmptyDocument() {
|
|
1913
|
+
return ContentModel.createEmpty();
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
class RichContentService {
|
|
1917
|
+
constructor(config2 = {}) {
|
|
1918
|
+
this.history = [];
|
|
1919
|
+
this.historyIndex = -1;
|
|
1920
|
+
this.adapter = null;
|
|
1921
|
+
this.defaultImageHandler = async (file) => {
|
|
1922
|
+
return new Promise((resolve) => {
|
|
1923
|
+
const reader = new FileReader();
|
|
1924
|
+
reader.onload = (e) => {
|
|
1925
|
+
var _a;
|
|
1926
|
+
resolve({ url: (_a = e.target) == null ? void 0 : _a.result });
|
|
1927
|
+
};
|
|
1928
|
+
reader.readAsDataURL(file);
|
|
1929
|
+
});
|
|
1930
|
+
};
|
|
1931
|
+
this.config = {
|
|
1932
|
+
initialContent: config2.initialContent || ContentModel.createEmpty(),
|
|
1933
|
+
allowedMarks: config2.allowedMarks || [
|
|
1934
|
+
"bold",
|
|
1935
|
+
"italic",
|
|
1936
|
+
"underline",
|
|
1937
|
+
"strikethrough",
|
|
1938
|
+
"code"
|
|
1939
|
+
],
|
|
1940
|
+
allowedBlocks: config2.allowedBlocks || [
|
|
1941
|
+
"document",
|
|
1942
|
+
"paragraph",
|
|
1943
|
+
"heading",
|
|
1944
|
+
"list",
|
|
1945
|
+
"blockquote",
|
|
1946
|
+
"code-block",
|
|
1947
|
+
"horizontal-rule"
|
|
1948
|
+
],
|
|
1949
|
+
maxListDepth: config2.maxListDepth ?? 3,
|
|
1950
|
+
imageUploadHandler: config2.imageUploadHandler || this.defaultImageHandler,
|
|
1951
|
+
mentionProvider: config2.mentionProvider || (() => Promise.resolve([])),
|
|
1952
|
+
middleware: config2.middleware || {},
|
|
1953
|
+
enableHistory: config2.enableHistory ?? true,
|
|
1954
|
+
maxHistorySteps: config2.maxHistorySteps ?? 100,
|
|
1955
|
+
placeholder: config2.placeholder || "",
|
|
1956
|
+
readOnly: config2.readOnly ?? false
|
|
1957
|
+
};
|
|
1958
|
+
this.middleware = this.config.middleware;
|
|
1959
|
+
this.contentModel = new ContentModel(this.config.initialContent);
|
|
1960
|
+
this.contentSubject = new BehaviorSubject(
|
|
1961
|
+
this.contentModel.getDocument()
|
|
1962
|
+
);
|
|
1963
|
+
this.commandSubject = new Subject$1();
|
|
1964
|
+
this.mentionSubject = new Subject$1();
|
|
1965
|
+
this.stateSubject = new BehaviorSubject({
|
|
1966
|
+
content: this.contentModel.getDocument(),
|
|
1967
|
+
selection: null,
|
|
1968
|
+
canUndo: false,
|
|
1969
|
+
canRedo: false,
|
|
1970
|
+
isFocused: false,
|
|
1971
|
+
isDirty: false,
|
|
1972
|
+
mentions: {
|
|
1973
|
+
isActive: false,
|
|
1974
|
+
query: "",
|
|
1975
|
+
position: { x: 0, y: 0 }
|
|
1976
|
+
},
|
|
1977
|
+
selectedFormats: /* @__PURE__ */ new Set()
|
|
1978
|
+
});
|
|
1979
|
+
if (this.config.enableHistory) {
|
|
1980
|
+
this.history.push({
|
|
1981
|
+
content: this.contentModel.getDocument(),
|
|
1982
|
+
timestamp: Date.now()
|
|
1983
|
+
});
|
|
1984
|
+
this.historyIndex = 0;
|
|
1985
|
+
}
|
|
1986
|
+
this.commandSubject.subscribe((cmd) => this.executeCommand(cmd));
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Create service from empty state
|
|
1990
|
+
*/
|
|
1991
|
+
static create(config2) {
|
|
1992
|
+
return new RichContentService(config2);
|
|
1993
|
+
}
|
|
1994
|
+
// =========================================================================
|
|
1995
|
+
// PUBLIC OBSERVABLES
|
|
1996
|
+
// =========================================================================
|
|
1997
|
+
/**
|
|
1998
|
+
* Get content$ observable
|
|
1999
|
+
*/
|
|
2000
|
+
getContent$() {
|
|
2001
|
+
return this.contentSubject.asObservable();
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Get full state$ observable
|
|
2005
|
+
*/
|
|
2006
|
+
getState$() {
|
|
2007
|
+
return this.stateSubject.asObservable();
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Get canUndo$ observable
|
|
2011
|
+
*/
|
|
2012
|
+
getCanUndo$() {
|
|
2013
|
+
return this.stateSubject.pipe(
|
|
2014
|
+
map((s) => s.canUndo),
|
|
2015
|
+
distinctUntilChanged()
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Get canRedo$ observable
|
|
2020
|
+
*/
|
|
2021
|
+
getCanRedo$() {
|
|
2022
|
+
return this.stateSubject.pipe(
|
|
2023
|
+
map((s) => s.canRedo),
|
|
2024
|
+
distinctUntilChanged()
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Get isFocused$ observable
|
|
2029
|
+
*/
|
|
2030
|
+
getIsFocused$() {
|
|
2031
|
+
return this.stateSubject.pipe(
|
|
2032
|
+
map((s) => s.isFocused),
|
|
2033
|
+
distinctUntilChanged()
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Get mentions$ observable - for autocomplete dropdown
|
|
2038
|
+
*/
|
|
2039
|
+
getMentions$() {
|
|
2040
|
+
return this.mentionSubject.asObservable();
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Get selectedFormats$ observable
|
|
2044
|
+
*/
|
|
2045
|
+
getSelectedFormats$() {
|
|
2046
|
+
return this.stateSubject.pipe(
|
|
2047
|
+
map((s) => s.selectedFormats || /* @__PURE__ */ new Set()),
|
|
2048
|
+
distinctUntilChanged()
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
// =========================================================================
|
|
2052
|
+
// STATE GETTERS
|
|
2053
|
+
// =========================================================================
|
|
2054
|
+
getContent() {
|
|
2055
|
+
return this.contentSubject.getValue();
|
|
2056
|
+
}
|
|
2057
|
+
getState() {
|
|
2058
|
+
return this.stateSubject.getValue();
|
|
2059
|
+
}
|
|
2060
|
+
getPlainText() {
|
|
2061
|
+
return this.contentModel.getPlainText();
|
|
2062
|
+
}
|
|
2063
|
+
canUndo() {
|
|
2064
|
+
return this.historyIndex > 0;
|
|
2065
|
+
}
|
|
2066
|
+
canRedo() {
|
|
2067
|
+
return this.historyIndex < this.history.length - 1;
|
|
2068
|
+
}
|
|
2069
|
+
isFocused() {
|
|
2070
|
+
return this.getState().isFocused;
|
|
2071
|
+
}
|
|
2072
|
+
// =========================================================================
|
|
2073
|
+
// ADAPTER MANAGEMENT
|
|
2074
|
+
// =========================================================================
|
|
2075
|
+
/**
|
|
2076
|
+
* Attach DOM adapter for contentEditable syncing
|
|
2077
|
+
*/
|
|
2078
|
+
attachAdapter(adapter) {
|
|
2079
|
+
this.adapter = adapter;
|
|
2080
|
+
const docFromDom = adapter.syncFromDOM();
|
|
2081
|
+
this.setContent(docFromDom);
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Detach adapter
|
|
2085
|
+
*/
|
|
2086
|
+
detachAdapter() {
|
|
2087
|
+
if (this.adapter) {
|
|
2088
|
+
this.adapter.unmount();
|
|
2089
|
+
this.adapter = null;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Get attached adapter
|
|
2094
|
+
*/
|
|
2095
|
+
getAdapter() {
|
|
2096
|
+
return this.adapter;
|
|
2097
|
+
}
|
|
2098
|
+
// =========================================================================
|
|
2099
|
+
// COMMAND EXECUTION
|
|
2100
|
+
// =========================================================================
|
|
2101
|
+
/**
|
|
2102
|
+
* Execute a command
|
|
2103
|
+
*/
|
|
2104
|
+
execute(command, payload) {
|
|
2105
|
+
const cmd = typeof command === "string" ? { type: command, payload } : command;
|
|
2106
|
+
this.commandSubject.next(cmd);
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Execute text insertion
|
|
2110
|
+
*/
|
|
2111
|
+
insertText(text, marks) {
|
|
2112
|
+
this.execute("insertText", { text, marks });
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Execute paragraph insertion
|
|
2116
|
+
*/
|
|
2117
|
+
insertParagraph() {
|
|
2118
|
+
this.execute("insertParagraph");
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Execute heading insertion
|
|
2122
|
+
*/
|
|
2123
|
+
insertHeading(level) {
|
|
2124
|
+
this.execute("insertHeading", { level });
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Execute image insertion
|
|
2128
|
+
*/
|
|
2129
|
+
insertImage(url, attrs) {
|
|
2130
|
+
this.execute("insertImage", { url, attrs });
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Execute image upload
|
|
2134
|
+
*/
|
|
2135
|
+
async uploadImage(file) {
|
|
2136
|
+
try {
|
|
2137
|
+
const result = await this.config.imageUploadHandler(file);
|
|
2138
|
+
this.insertImage(result.url, result.attrs);
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
console.error("Image upload failed:", error);
|
|
2141
|
+
throw error;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Execute mention insertion
|
|
2146
|
+
*/
|
|
2147
|
+
insertMention(id, label, meta) {
|
|
2148
|
+
this.execute("insertMention", { id, label, meta });
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Execute list insertion
|
|
2152
|
+
*/
|
|
2153
|
+
insertList(listType) {
|
|
2154
|
+
this.execute("insertList", { listType });
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Execute mark toggle (bold, italic, etc.)
|
|
2158
|
+
*/
|
|
2159
|
+
toggleMark(mark) {
|
|
2160
|
+
this.execute("toggleMark", { mark });
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Delete content
|
|
2164
|
+
*/
|
|
2165
|
+
deleteContent() {
|
|
2166
|
+
this.execute("deleteContent");
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Set focus state
|
|
2170
|
+
*/
|
|
2171
|
+
setFocus(focused) {
|
|
2172
|
+
const state = this.getState();
|
|
2173
|
+
state.isFocused = focused;
|
|
2174
|
+
this.stateSubject.next(state);
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Set selection
|
|
2178
|
+
*/
|
|
2179
|
+
setSelection(selection) {
|
|
2180
|
+
const state = this.getState();
|
|
2181
|
+
state.selection = selection;
|
|
2182
|
+
this.stateSubject.next(state);
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Set mention query (triggers autocomplete)
|
|
2186
|
+
*/
|
|
2187
|
+
setMentionQuery(query, position) {
|
|
2188
|
+
if (query.length > 0) {
|
|
2189
|
+
this.mentionSubject.next({ query, position });
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
// =========================================================================
|
|
2193
|
+
// HISTORY (UNDO/REDO)
|
|
2194
|
+
// =========================================================================
|
|
2195
|
+
/**
|
|
2196
|
+
* Undo to previous state
|
|
2197
|
+
*/
|
|
2198
|
+
undo() {
|
|
2199
|
+
if (this.canUndo()) {
|
|
2200
|
+
this.historyIndex--;
|
|
2201
|
+
this.applyHistoryState();
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Redo to next state
|
|
2206
|
+
*/
|
|
2207
|
+
redo() {
|
|
2208
|
+
if (this.canRedo()) {
|
|
2209
|
+
this.historyIndex++;
|
|
2210
|
+
this.applyHistoryState();
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Clear history
|
|
2215
|
+
*/
|
|
2216
|
+
clearHistory() {
|
|
2217
|
+
this.history = [{ content: this.getContent(), timestamp: Date.now() }];
|
|
2218
|
+
this.historyIndex = 0;
|
|
2219
|
+
this.updateHistoryState();
|
|
2220
|
+
}
|
|
2221
|
+
// =========================================================================
|
|
2222
|
+
// PRIVATE METHODS
|
|
2223
|
+
// =========================================================================
|
|
2224
|
+
executeCommand(cmd) {
|
|
2225
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
|
|
2226
|
+
if (this.config.readOnly) {
|
|
2227
|
+
console.warn("Editor is in read-only mode");
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
this.getContent();
|
|
2231
|
+
let newContent;
|
|
2232
|
+
switch (cmd.type) {
|
|
2233
|
+
case "insertText":
|
|
2234
|
+
newContent = this.contentModel.insertText((_a = cmd.payload) == null ? void 0 : _a.text, (_b = cmd.payload) == null ? void 0 : _b.marks);
|
|
2235
|
+
break;
|
|
2236
|
+
case "insertParagraph":
|
|
2237
|
+
newContent = this.contentModel.insertParagraph((_c = cmd.payload) == null ? void 0 : _c.children);
|
|
2238
|
+
break;
|
|
2239
|
+
case "insertHeading":
|
|
2240
|
+
newContent = this.contentModel.insertHeading(
|
|
2241
|
+
((_d = cmd.payload) == null ? void 0 : _d.level) || 1,
|
|
2242
|
+
(_e = cmd.payload) == null ? void 0 : _e.children
|
|
2243
|
+
);
|
|
2244
|
+
break;
|
|
2245
|
+
case "insertImage":
|
|
2246
|
+
newContent = this.contentModel.insertImage((_f = cmd.payload) == null ? void 0 : _f.url, (_g = cmd.payload) == null ? void 0 : _g.attrs);
|
|
2247
|
+
break;
|
|
2248
|
+
case "insertMention":
|
|
2249
|
+
newContent = this.contentModel.insertMention(
|
|
2250
|
+
(_h = cmd.payload) == null ? void 0 : _h.id,
|
|
2251
|
+
(_i = cmd.payload) == null ? void 0 : _i.label,
|
|
2252
|
+
(_j = cmd.payload) == null ? void 0 : _j.meta
|
|
2253
|
+
);
|
|
2254
|
+
break;
|
|
2255
|
+
case "insertList":
|
|
2256
|
+
newContent = this.contentModel.insertList((_k = cmd.payload) == null ? void 0 : _k.listType, (_l = cmd.payload) == null ? void 0 : _l.items);
|
|
2257
|
+
break;
|
|
2258
|
+
case "toggleMark":
|
|
2259
|
+
newContent = this.contentModel.toggleMark((_m = cmd.payload) == null ? void 0 : _m.mark, (_n = cmd.payload) == null ? void 0 : _n.selection);
|
|
2260
|
+
break;
|
|
2261
|
+
case "deleteContent":
|
|
2262
|
+
newContent = this.contentModel.deleteContent((_o = cmd.payload) == null ? void 0 : _o.selection);
|
|
2263
|
+
break;
|
|
2264
|
+
case "replaceContent":
|
|
2265
|
+
newContent = this.contentModel.replaceContent((_p = cmd.payload) == null ? void 0 : _p.nodes);
|
|
2266
|
+
break;
|
|
2267
|
+
default:
|
|
2268
|
+
console.warn(`Unknown command type: ${cmd.type}`);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
if (this.config.enableHistory) {
|
|
2272
|
+
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
2273
|
+
this.history.push({
|
|
2274
|
+
content: newContent,
|
|
2275
|
+
timestamp: Date.now()
|
|
2276
|
+
});
|
|
2277
|
+
if (this.history.length > this.config.maxHistorySteps) {
|
|
2278
|
+
this.history.shift();
|
|
2279
|
+
} else {
|
|
2280
|
+
this.historyIndex++;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
this.setContent(newContent);
|
|
2284
|
+
}
|
|
2285
|
+
setContent(doc) {
|
|
2286
|
+
this.contentModel.setDocument(doc);
|
|
2287
|
+
this.contentSubject.next(doc);
|
|
2288
|
+
const state = this.getState();
|
|
2289
|
+
state.content = doc;
|
|
2290
|
+
state.isDirty = true;
|
|
2291
|
+
state.selectedFormats = new Set(this.contentModel.getActiveMarks());
|
|
2292
|
+
this.stateSubject.next(state);
|
|
2293
|
+
if (this.adapter) {
|
|
2294
|
+
this.adapter.syncToDOM(doc);
|
|
2295
|
+
}
|
|
2296
|
+
this.updateHistoryState();
|
|
2297
|
+
}
|
|
2298
|
+
applyHistoryState() {
|
|
2299
|
+
const entry = this.history[this.historyIndex];
|
|
2300
|
+
this.contentModel.setDocument(entry.content);
|
|
2301
|
+
this.contentSubject.next(entry.content);
|
|
2302
|
+
const state = this.getState();
|
|
2303
|
+
state.content = entry.content;
|
|
2304
|
+
this.stateSubject.next(state);
|
|
2305
|
+
if (this.adapter) {
|
|
2306
|
+
this.adapter.syncToDOM(entry.content);
|
|
2307
|
+
}
|
|
2308
|
+
this.updateHistoryState();
|
|
2309
|
+
}
|
|
2310
|
+
updateHistoryState() {
|
|
2311
|
+
const state = this.getState();
|
|
2312
|
+
state.canUndo = this.canUndo();
|
|
2313
|
+
state.canRedo = this.canRedo();
|
|
2314
|
+
this.stateSubject.next(state);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
function useRichContent(options = {}) {
|
|
2318
|
+
const service = useMemo(() => RichContentService.create(options), []);
|
|
2319
|
+
const [content, setContent] = useState(service.getContent());
|
|
2320
|
+
const [state, setState] = useState(service.getState());
|
|
2321
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
2322
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
2323
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
2324
|
+
const [selectedFormats, setSelectedFormats] = useState(/* @__PURE__ */ new Set());
|
|
2325
|
+
useEffect(() => {
|
|
2326
|
+
const contentSub = service.getContent$().subscribe(setContent);
|
|
2327
|
+
const stateSub = service.getState$().subscribe((newState) => {
|
|
2328
|
+
setState(newState);
|
|
2329
|
+
setIsFocused(newState.isFocused);
|
|
2330
|
+
setCanUndo(newState.canUndo);
|
|
2331
|
+
setCanRedo(newState.canRedo);
|
|
2332
|
+
setSelectedFormats(newState.selectedFormats || /* @__PURE__ */ new Set());
|
|
2333
|
+
});
|
|
2334
|
+
return () => {
|
|
2335
|
+
contentSub.unsubscribe();
|
|
2336
|
+
stateSub.unsubscribe();
|
|
2337
|
+
};
|
|
2338
|
+
}, [service]);
|
|
2339
|
+
useEffect(() => {
|
|
2340
|
+
if (options.adapter) {
|
|
2341
|
+
service.attachAdapter(options.adapter);
|
|
2342
|
+
return () => service.detachAdapter();
|
|
2343
|
+
}
|
|
2344
|
+
}, [options.adapter, service]);
|
|
2345
|
+
const defaultAdapterRef = useRef(null);
|
|
2346
|
+
useEffect(() => {
|
|
2347
|
+
var _a;
|
|
2348
|
+
if (((_a = options.editorRef) == null ? void 0 : _a.current) && !options.adapter && !defaultAdapterRef.current) {
|
|
2349
|
+
const { DefaultContentEditableAdapter } = require("@codella/core/rich-content");
|
|
2350
|
+
const adapter = new DefaultContentEditableAdapter();
|
|
2351
|
+
adapter.mount(options.editorRef.current);
|
|
2352
|
+
service.attachAdapter(adapter);
|
|
2353
|
+
defaultAdapterRef.current = adapter;
|
|
2354
|
+
return () => {
|
|
2355
|
+
adapter.unmount();
|
|
2356
|
+
service.detachAdapter();
|
|
2357
|
+
defaultAdapterRef.current = null;
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
}, [options.editorRef, options.adapter, service]);
|
|
2361
|
+
const insertText = useCallback((text) => service.insertText(text), [service]);
|
|
2362
|
+
const insertParagraph = useCallback(() => service.insertParagraph(), [service]);
|
|
2363
|
+
const insertHeading = useCallback((level) => service.insertHeading(level), [service]);
|
|
2364
|
+
const insertImage = useCallback((url) => service.insertImage(url), [service]);
|
|
2365
|
+
const uploadImage = useCallback((file) => service.uploadImage(file), [service]);
|
|
2366
|
+
const insertMention = useCallback((id, label) => service.insertMention(id, label), [service]);
|
|
2367
|
+
const insertList = useCallback((type) => service.insertList(type), [service]);
|
|
2368
|
+
const toggleMark = useCallback((mark) => service.toggleMark(mark), [service]);
|
|
2369
|
+
const deleteContent = useCallback(() => service.deleteContent(), [service]);
|
|
2370
|
+
const undo = useCallback(() => service.undo(), [service]);
|
|
2371
|
+
const redo = useCallback(() => service.redo(), [service]);
|
|
2372
|
+
const clearHistory = useCallback(() => service.clearHistory(), [service]);
|
|
2373
|
+
const focus = useCallback(() => {
|
|
2374
|
+
var _a;
|
|
2375
|
+
return (_a = service.getAdapter()) == null ? void 0 : _a.focus();
|
|
2376
|
+
}, [service]);
|
|
2377
|
+
const setFocus = useCallback((focused) => service.setFocus(focused), [service]);
|
|
2378
|
+
const getPlainText = useCallback(() => service.getPlainText(), [service]);
|
|
2379
|
+
const setSelection = useCallback((sel) => service.setSelection(sel), [service]);
|
|
2380
|
+
return {
|
|
2381
|
+
service,
|
|
2382
|
+
content,
|
|
2383
|
+
state,
|
|
2384
|
+
isFocused,
|
|
2385
|
+
canUndo,
|
|
2386
|
+
canRedo,
|
|
2387
|
+
selectedFormats,
|
|
2388
|
+
selection: state.selection || null,
|
|
2389
|
+
isDirty: state.isDirty,
|
|
2390
|
+
insertText,
|
|
2391
|
+
insertParagraph,
|
|
2392
|
+
insertHeading,
|
|
2393
|
+
insertImage,
|
|
2394
|
+
uploadImage,
|
|
2395
|
+
insertMention,
|
|
2396
|
+
insertList,
|
|
2397
|
+
toggleMark,
|
|
2398
|
+
deleteContent,
|
|
2399
|
+
undo,
|
|
2400
|
+
redo,
|
|
2401
|
+
clearHistory,
|
|
2402
|
+
focus,
|
|
2403
|
+
setFocus,
|
|
2404
|
+
getPlainText,
|
|
2405
|
+
setSelection
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
1632
2408
|
function useTableService(options) {
|
|
1633
2409
|
const { config: config2, data } = options;
|
|
1634
2410
|
const builder = useMemo(() => {
|
|
@@ -1992,6 +2768,7 @@ export {
|
|
|
1992
2768
|
useLiveUpdateContext,
|
|
1993
2769
|
useLiveUpdateListener,
|
|
1994
2770
|
useLiveUpdates,
|
|
2771
|
+
useRichContent,
|
|
1995
2772
|
useSetActiveTab,
|
|
1996
2773
|
useTabChange,
|
|
1997
2774
|
useTableService,
|