From 40c5262a9c2fcb8c89d7c464ccfdc3016f933eb3 Mon Sep 17 00:00:00 2001
From: Giuseppe <Giuseppe06@gmail.com>
Date: Sat, 21 Dec 2024 14:09:30 +0100
Subject: feature: WYSIWYG markdown for notes. Fixes #701 (#715)

* #701 Improve note support : WYSIWYG markdown

First implementation with a wysiwyg markdown editor.
Update:
- Add Lexical markdown editor
- consistent rendering between card and preview
- removed edit modal, replaced by preview with save action
- simple markdown shortcut: underline, bold, italic etc...

* #701 Improve note support : WYSIWYG markdown

improved performance to not rerender all note card when one is updated

* Use markdown shortcuts

* Remove the alignment actions

* Drop history buttons

* Fix code and highlighting buttons

* Remove the unneeded update markdown plugin

* Remove underline support as it's not markdown native

* - added ListPlugin because if absent, there's a bug where you can't escape a list with enter + enter
    - added codeblock plugin
    - added prose dark:prose-invert prose-p:m-0 like you said (there's room for improvement I think, don't took the time too deep dive in) and removed theme
    - Added a switch to show raw markdown
    - Added back the react markdown for card (SSR)

* delete theme.ts

* add theme back for code element to be more like prism theme from markdown-readonly

* move the new editor back to the edit menu

* move the bookmark markdown component into dashboard/bookmark

* move the tooltip into its own component

* move save button to toolbar

* Better raw markdown

---------

Co-authored-by: Giuseppe Lapenta <giuseppe.lapenta@enovacom.com>
Co-authored-by: Mohamed Bassem <me@mbassem.com>
---
 .../web/components/ui/markdown/markdown-editor.tsx | 124 +++++++++++++++++++++
 1 file changed, 124 insertions(+)
 create mode 100644 apps/web/components/ui/markdown/markdown-editor.tsx

(limited to 'apps/web/components/ui/markdown/markdown-editor.tsx')

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;
-- 
cgit v1.2.3-70-g09d2

