diff options
Diffstat (limited to 'apps/web/components/ui/markdown/markdown-editor.tsx')
| -rw-r--r-- | apps/web/components/ui/markdown/markdown-editor.tsx | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/apps/web/components/ui/markdown/markdown-editor.tsx b/apps/web/components/ui/markdown/markdown-editor.tsx new file mode 100644 index 00000000..85f0c878 --- /dev/null +++ b/apps/web/components/ui/markdown/markdown-editor.tsx @@ -0,0 +1,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; |
