@codella-software/react 2.0.1 → 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/README.md +36 -4
- package/dist/index.cjs +777 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +779 -121
- 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/dist/react/src/tabs/index.d.ts +5 -7
- package/dist/react/src/tabs/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/react/src/tabs/ResponsiveTabs.d.ts +0 -50
- package/dist/react/src/tabs/ResponsiveTabs.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
import { jsx
|
|
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";
|
|
5
5
|
import { takeUntil, map, filter, tap, take, distinctUntilChanged } from "rxjs/operators";
|
|
6
6
|
function useFiltersAndSort(options) {
|
|
@@ -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(() => {
|
|
@@ -1981,128 +2757,9 @@ function useSetActiveTab() {
|
|
|
1981
2757
|
previousTab: () => service.previousTab()
|
|
1982
2758
|
};
|
|
1983
2759
|
}
|
|
1984
|
-
function ResponsiveTabs({
|
|
1985
|
-
className = "",
|
|
1986
|
-
tabClassName = "",
|
|
1987
|
-
activeTabClassName = "",
|
|
1988
|
-
selectClassName = "",
|
|
1989
|
-
breakpoint = 768,
|
|
1990
|
-
showBadges = true,
|
|
1991
|
-
icon = true
|
|
1992
|
-
}) {
|
|
1993
|
-
const [isMobile, setIsMobile] = useState(false);
|
|
1994
|
-
const activeTab = useActiveTab();
|
|
1995
|
-
const tabs = useTabs();
|
|
1996
|
-
const { setActiveTab } = useSetActiveTab();
|
|
1997
|
-
useEffect(() => {
|
|
1998
|
-
const handleResize = () => {
|
|
1999
|
-
setIsMobile(window.innerWidth < breakpoint);
|
|
2000
|
-
};
|
|
2001
|
-
handleResize();
|
|
2002
|
-
window.addEventListener("resize", handleResize);
|
|
2003
|
-
return () => {
|
|
2004
|
-
window.removeEventListener("resize", handleResize);
|
|
2005
|
-
};
|
|
2006
|
-
}, [breakpoint]);
|
|
2007
|
-
if (!activeTab) {
|
|
2008
|
-
return null;
|
|
2009
|
-
}
|
|
2010
|
-
return isMobile ? /* @__PURE__ */ jsx(
|
|
2011
|
-
TabsSelect,
|
|
2012
|
-
{
|
|
2013
|
-
tabs,
|
|
2014
|
-
activeTabId: activeTab.id,
|
|
2015
|
-
onTabChange: setActiveTab,
|
|
2016
|
-
className: selectClassName
|
|
2017
|
-
}
|
|
2018
|
-
) : /* @__PURE__ */ jsx(
|
|
2019
|
-
TabsList,
|
|
2020
|
-
{
|
|
2021
|
-
tabs,
|
|
2022
|
-
activeTabId: activeTab.id,
|
|
2023
|
-
onTabChange: setActiveTab,
|
|
2024
|
-
className,
|
|
2025
|
-
tabClassName,
|
|
2026
|
-
activeTabClassName,
|
|
2027
|
-
showBadges,
|
|
2028
|
-
icon
|
|
2029
|
-
}
|
|
2030
|
-
);
|
|
2031
|
-
}
|
|
2032
|
-
function TabsList({
|
|
2033
|
-
tabs,
|
|
2034
|
-
activeTabId,
|
|
2035
|
-
onTabChange,
|
|
2036
|
-
className = "",
|
|
2037
|
-
tabClassName = "",
|
|
2038
|
-
activeTabClassName = "",
|
|
2039
|
-
showBadges = true,
|
|
2040
|
-
icon = true
|
|
2041
|
-
}) {
|
|
2042
|
-
return /* @__PURE__ */ jsx(
|
|
2043
|
-
"div",
|
|
2044
|
-
{
|
|
2045
|
-
role: "tablist",
|
|
2046
|
-
className: `flex border-b border-gray-200 ${className}`,
|
|
2047
|
-
"aria-label": "Tabs",
|
|
2048
|
-
children: tabs.map((tab) => /* @__PURE__ */ jsx(
|
|
2049
|
-
"button",
|
|
2050
|
-
{
|
|
2051
|
-
role: "tab",
|
|
2052
|
-
"aria-selected": tab.id === activeTabId,
|
|
2053
|
-
"aria-controls": `tabpanel-${tab.id}`,
|
|
2054
|
-
id: `tab-${tab.id}`,
|
|
2055
|
-
onClick: () => !tab.disabled && onTabChange(tab.id),
|
|
2056
|
-
disabled: tab.disabled,
|
|
2057
|
-
className: `
|
|
2058
|
-
relative px-4 py-2 text-sm font-medium transition-colors
|
|
2059
|
-
${tab.disabled ? "text-gray-400 cursor-not-allowed" : "text-gray-700 hover:text-gray-900"}
|
|
2060
|
-
${tab.id === activeTabId ? `text-blue-600 border-b-2 border-blue-600 ${activeTabClassName}` : ""}
|
|
2061
|
-
${tabClassName}
|
|
2062
|
-
`,
|
|
2063
|
-
children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
2064
|
-
icon && tab.icon && /* @__PURE__ */ jsx("span", { className: "text-lg", children: tab.icon }),
|
|
2065
|
-
/* @__PURE__ */ jsx("span", { children: tab.label }),
|
|
2066
|
-
showBadges && tab.badge && /* @__PURE__ */ jsx("span", { className: "ml-1 inline-flex items-center justify-center px-2 py-1 text-xs font-semibold leading-none text-white transform translate-y-px bg-red-600 rounded-full", children: tab.badge })
|
|
2067
|
-
] })
|
|
2068
|
-
},
|
|
2069
|
-
tab.id
|
|
2070
|
-
))
|
|
2071
|
-
}
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2074
|
-
function TabsSelect({
|
|
2075
|
-
tabs,
|
|
2076
|
-
activeTabId,
|
|
2077
|
-
onTabChange,
|
|
2078
|
-
className = ""
|
|
2079
|
-
}) {
|
|
2080
|
-
tabs.find((t) => t.id === activeTabId);
|
|
2081
|
-
return /* @__PURE__ */ jsx("div", { className: `w-full ${className}`, children: /* @__PURE__ */ jsx(
|
|
2082
|
-
"select",
|
|
2083
|
-
{
|
|
2084
|
-
value: activeTabId,
|
|
2085
|
-
onChange: (e) => onTabChange(e.target.value),
|
|
2086
|
-
"aria-label": "Select a tab",
|
|
2087
|
-
className: `
|
|
2088
|
-
w-full px-3 py-2 text-sm font-medium border border-gray-300 rounded-md
|
|
2089
|
-
bg-white text-gray-900 hover:border-gray-400 focus:outline-none focus:ring-2
|
|
2090
|
-
focus:ring-blue-500 focus:ring-offset-0
|
|
2091
|
-
`,
|
|
2092
|
-
children: tabs.map((tab) => /* @__PURE__ */ jsxs("option", { value: tab.id, disabled: tab.disabled, children: [
|
|
2093
|
-
tab.label,
|
|
2094
|
-
" ",
|
|
2095
|
-
tab.badge ? `(${tab.badge})` : ""
|
|
2096
|
-
] }, tab.id))
|
|
2097
|
-
}
|
|
2098
|
-
) });
|
|
2099
|
-
}
|
|
2100
2760
|
export {
|
|
2101
2761
|
LiveUpdateProvider,
|
|
2102
|
-
ResponsiveTabs,
|
|
2103
|
-
TabsList,
|
|
2104
2762
|
TabsProvider,
|
|
2105
|
-
TabsSelect,
|
|
2106
2763
|
useActiveTab,
|
|
2107
2764
|
useFiltersAndSort,
|
|
2108
2765
|
useFormBuilder,
|
|
@@ -2111,6 +2768,7 @@ export {
|
|
|
2111
2768
|
useLiveUpdateContext,
|
|
2112
2769
|
useLiveUpdateListener,
|
|
2113
2770
|
useLiveUpdates,
|
|
2771
|
+
useRichContent,
|
|
2114
2772
|
useSetActiveTab,
|
|
2115
2773
|
useTabChange,
|
|
2116
2774
|
useTableService,
|