1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
import { memo, useMemo, useState } from "react";
import ToolbarPlugin from "@/components/ui/markdown/plugins/toolbar-plugin";
import { MarkdownEditorTheme } from "@/components/ui/markdown/theme";
import {
CodeHighlightNode,
CodeNode,
registerCodeHighlighting,
} from "@lexical/code";
import { LinkNode } from "@lexical/link";
import { ListItemNode, ListNode } from "@lexical/list";
import {
$convertFromMarkdownString,
$convertToMarkdownString,
TRANSFORMERS,
} from "@lexical/markdown";
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import {
InitialConfigType,
LexicalComposer,
} from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
import { $getRoot, EditorState, LexicalEditor } from "lexical";
function onError(error: Error) {
console.error(error);
}
const EDITOR_NODES = [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
LinkNode,
CodeNode,
HorizontalRuleNode,
CodeHighlightNode,
];
interface MarkdownEditorProps {
children: string;
onSave?: (markdown: string) => void;
isSaving?: boolean;
}
const MarkdownEditor = memo(
({ children: initialMarkdown, onSave, isSaving }: MarkdownEditorProps) => {
const [isRawMarkdownMode, setIsRawMarkdownMode] = useState(false);
const [rawMarkdown, setRawMarkdown] = useState(initialMarkdown);
const initialConfig: InitialConfigType = useMemo(
() => ({
namespace: "editor",
onError,
theme: MarkdownEditorTheme,
nodes: EDITOR_NODES,
editorState: (editor: LexicalEditor) => {
registerCodeHighlighting(editor);
$convertFromMarkdownString(initialMarkdown, TRANSFORMERS);
},
}),
[initialMarkdown],
);
const handleOnChange = (editorState: EditorState) => {
editorState.read(() => {
let markdownString;
if (isRawMarkdownMode) {
markdownString = $getRoot()?.getFirstChild()?.getTextContent() ?? "";
} else {
markdownString = $convertToMarkdownString(TRANSFORMERS);
}
setRawMarkdown(markdownString);
});
};
return (
<LexicalComposer initialConfig={initialConfig}>
<div className="flex h-full flex-col justify-stretch">
<ToolbarPlugin
isRawMarkdownMode={isRawMarkdownMode}
setIsRawMarkdownMode={setIsRawMarkdownMode}
onSave={onSave && (() => onSave(rawMarkdown))}
isSaving={!!isSaving}
/>
{isRawMarkdownMode ? (
<PlainTextPlugin
contentEditable={
<ContentEditable className="h-full w-full min-w-full overflow-auto p-2" />
}
ErrorBoundary={LexicalErrorBoundary}
/>
) : (
<RichTextPlugin
contentEditable={
<ContentEditable className="prose h-full w-full min-w-full overflow-auto p-2 dark:prose-invert prose-p:m-0" />
}
ErrorBoundary={LexicalErrorBoundary}
/>
)}
</div>
<HistoryPlugin />
<AutoFocusPlugin />
<TabIndentationPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<OnChangePlugin onChange={handleOnChange} />
<ListPlugin />
</LexicalComposer>
);
},
);
// needed for linter because of memo
MarkdownEditor.displayName = "MarkdownEditor";
export default MarkdownEditor;
|