aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/ui/markdown/markdown-editor.tsx
blob: 85f0c8788c9d5d2f611b83502700e567b85cbe60 (plain) (blame)
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;