aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/ui/markdown/markdown-editor.tsx
diff options
context:
space:
mode:
authorGiuseppe <Giuseppe06@gmail.com>2024-12-21 14:09:30 +0100
committerGitHub <noreply@github.com>2024-12-21 15:09:30 +0200
commit40c5262a9c2fcb8c89d7c464ccfdc3016f933eb3 (patch)
tree108852741fc06c28b851563738f499cdaccdf6bd /apps/web/components/ui/markdown/markdown-editor.tsx
parent16f2ce378b121dcbea31708630def8ff95e90f14 (diff)
downloadkarakeep-40c5262a9c2fcb8c89d7c464ccfdc3016f933eb3.tar.zst
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>
Diffstat (limited to 'apps/web/components/ui/markdown/markdown-editor.tsx')
-rw-r--r--apps/web/components/ui/markdown/markdown-editor.tsx124
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;