diff options
| author | Giuseppe <Giuseppe06@gmail.com> | 2024-12-21 14:09:30 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-12-21 15:09:30 +0200 |
| commit | 40c5262a9c2fcb8c89d7c464ccfdc3016f933eb3 (patch) | |
| tree | 108852741fc06c28b851563738f499cdaccdf6bd /apps/web/components/ui/markdown/markdown-editor.tsx | |
| parent | 16f2ce378b121dcbea31708630def8ff95e90f14 (diff) | |
| download | karakeep-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.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; |
