forked from hoarder-app/hoarder
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
hoarder-app#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...
- Loading branch information
Giuseppe Lapenta
committed
Dec 4, 2024
1 parent
5a49691
commit 08c27a7
Showing
12 changed files
with
813 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 0 additions & 81 deletions
81
apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,59 @@ | ||
import React from "react"; | ||
import CopyBtn from "@/components/ui/copy-button"; | ||
import { cn } from "@/lib/utils"; | ||
import Markdown from "react-markdown"; | ||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; | ||
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism"; | ||
import remarkBreaks from "remark-breaks"; | ||
import remarkGfm from "remark-gfm"; | ||
import React, { useState } from "react"; | ||
import { ActionButton } from "@/components/ui/action-button"; | ||
import MarkdownEditor from "@/components/ui/markdown/markdown-editor"; | ||
import { toast } from "@/components/ui/use-toast"; | ||
|
||
function PreWithCopyBtn({ className, ...props }: React.ComponentProps<"pre">) { | ||
const ref = React.useRef<HTMLPreElement>(null); | ||
return ( | ||
<span className="group relative"> | ||
<CopyBtn | ||
className="absolute right-1 top-1 m-1 hidden text-white group-hover:block" | ||
getStringToCopy={() => { | ||
return ref.current?.textContent ?? ""; | ||
}} | ||
/> | ||
<pre ref={ref} className={cn(className, "")} {...props} /> | ||
</span> | ||
); | ||
} | ||
import type { ZBookmarkTypeText } from "@hoarder/shared/types/bookmarks"; | ||
import { useUpdateBookmarkText } from "@hoarder/shared-react/hooks/bookmarks"; | ||
|
||
export function MarkdownComponent({ | ||
children: markdown, | ||
children: bookmark, | ||
readOnly = true, | ||
}: { | ||
children: string; | ||
children: ZBookmarkTypeText; | ||
readOnly?: boolean; | ||
}) { | ||
const [noteText, setNoteText] = useState(bookmark.content.text); | ||
|
||
const { mutate: updateBookmarkMutator, isPending } = useUpdateBookmarkText({ | ||
onSuccess: () => { | ||
toast({ | ||
description: "Note updated!", | ||
}); | ||
}, | ||
onError: () => { | ||
toast({ description: "Something went wrong", variant: "destructive" }); | ||
}, | ||
}); | ||
|
||
const onSave = () => { | ||
updateBookmarkMutator({ | ||
bookmarkId: bookmark.id, | ||
text: noteText, | ||
}); | ||
}; | ||
return ( | ||
<Markdown | ||
remarkPlugins={[remarkGfm, remarkBreaks]} | ||
className="prose dark:prose-invert" | ||
components={{ | ||
pre({ ...props }) { | ||
return <PreWithCopyBtn {...props} />; | ||
}, | ||
code({ className, children, ...props }) { | ||
const match = /language-(\w+)/.exec(className ?? ""); | ||
return match ? ( | ||
// @ts-expect-error -- Refs are not compatible for some reason | ||
<SyntaxHighlighter | ||
PreTag="div" | ||
language={match[1]} | ||
{...props} | ||
style={dracula} | ||
> | ||
{String(children).replace(/\n$/, "")} | ||
</SyntaxHighlighter> | ||
) : ( | ||
<code className={className} {...props}> | ||
{children} | ||
</code> | ||
); | ||
}, | ||
}} | ||
> | ||
{markdown} | ||
</Markdown> | ||
<> | ||
<MarkdownEditor | ||
readonly={readOnly} | ||
onChangeMarkdown={(value: string) => { | ||
setNoteText(value); | ||
}} | ||
> | ||
{bookmark.content.text} | ||
</MarkdownEditor> | ||
{!readOnly && ( | ||
<div className="absolute bottom-2 right-2"> | ||
<ActionButton | ||
type="button" | ||
loading={isPending} | ||
onClick={onSave} | ||
disabled={isPending} | ||
> | ||
Save | ||
</ActionButton> | ||
</div> | ||
)} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import React, { useEffect, useState } from "react"; | ||
import ToolbarPlugin from "@/components/ui/markdown/plugins/tollbar-plugin"; | ||
import { UpdateMarkdownPlugin } from "@/components/ui/markdown/plugins/update-markdown-editor-plugin"; | ||
import { MarkdownEditorTheme } from "@/components/ui/markdown/theme/theme"; | ||
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 { 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 { EditorState } from "lexical"; | ||
|
||
function onError(error: Error) { | ||
console.error(error); | ||
} | ||
|
||
const EDITOR_NODES = [HeadingNode, ListNode, ListItemNode, QuoteNode, LinkNode]; | ||
|
||
interface MarkdownEditorProps { | ||
children: string; | ||
onChangeMarkdown?: (markdown: string) => void; | ||
readonly?: boolean; | ||
} | ||
|
||
const MarkdownEditor = ({ | ||
children: initialMarkdown, | ||
onChangeMarkdown, | ||
readonly = false, | ||
}: MarkdownEditorProps) => { | ||
const [markdown, setMarkdown] = useState<string | null>(null); | ||
|
||
useEffect(() => { | ||
setMarkdown(initialMarkdown); | ||
}, [initialMarkdown]); | ||
|
||
if (markdown === null) { | ||
return <div>Loading...</div>; | ||
} | ||
|
||
const initialConfig: InitialConfigType = { | ||
namespace: "editor", | ||
onError, | ||
editable: !readonly, | ||
theme: MarkdownEditorTheme, | ||
nodes: EDITOR_NODES, | ||
editorState: () => $convertFromMarkdownString(markdown, TRANSFORMERS), | ||
}; | ||
|
||
return ( | ||
<LexicalComposer initialConfig={initialConfig}> | ||
{readonly ? ( | ||
<PlainTextPlugin | ||
contentEditable={ | ||
<ContentEditable className="h-full w-full content-center" /> | ||
} | ||
ErrorBoundary={LexicalErrorBoundary} | ||
></PlainTextPlugin> | ||
) : ( | ||
<> | ||
<div className="flex h-full flex-col justify-stretch"> | ||
<ToolbarPlugin></ToolbarPlugin> | ||
<RichTextPlugin | ||
contentEditable={<ContentEditable className="h-full" />} | ||
ErrorBoundary={LexicalErrorBoundary} | ||
/> | ||
</div> | ||
</> | ||
)} | ||
{!readonly && ( | ||
<> | ||
<HistoryPlugin /> | ||
<AutoFocusPlugin /> | ||
<TabIndentationPlugin /> | ||
<OnChangePlugin | ||
onChange={(editorState: EditorState) => { | ||
editorState.read(() => { | ||
const markdownString = $convertToMarkdownString(); | ||
if (onChangeMarkdown) onChangeMarkdown(markdownString); | ||
}); | ||
}} | ||
/> | ||
</> | ||
)} | ||
<UpdateMarkdownPlugin markdown={markdown} /> | ||
</LexicalComposer> | ||
); | ||
}; | ||
|
||
export default MarkdownEditor; |
Oops, something went wrong.