From c3b8847a3d636c9a099d863fd6ff4efa04b6057c Mon Sep 17 00:00:00 2001 From: forhappy Date: Fri, 12 Jan 2024 23:48:45 -0800 Subject: [PATCH] feat: insertMarkdown method and signal (#294) The method inserts the given markdown content at the current selection point. --- docs/getting-started.md | 14 +++ src/MDXEditor.tsx | 11 +- src/examples/insert-markdown.tsx | 188 +++++++++++++++++++++++++++++++ src/importMarkdownToLexical.ts | 9 +- src/plugins/core/index.ts | 41 ++++++- 5 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 src/examples/insert-markdown.tsx diff --git a/docs/getting-started.md b/docs/getting-started.md index a0d6eb80..1fe1dd86 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -171,6 +171,20 @@ return ( ) ``` +If you want to insert markdown content after the editor is initialized, you can use `insertMarkdown` ref method to insert markdown content to the current cursor position of the active editor. + +```tsx +// create a ref to the editor component +const ref = React.useRef(null) +return ( + <> + + + + +) +``` + ## Next steps Hopefully, at this point, the editor component is installed and working in your setup, but it's not very useful. Depending on your use case, you will need some additional features. To ensure that the bundle size stays small, MDXEditor uses a plugin system. Below is an example of a few basic plugins being enabled for the editor. diff --git a/src/MDXEditor.tsx b/src/MDXEditor.tsx index bc7f6b36..88fbb9fa 100644 --- a/src/MDXEditor.tsx +++ b/src/MDXEditor.tsx @@ -14,7 +14,8 @@ import { viewMode$, markdown$, setMarkdown$, - rootEditor$ + rootEditor$, + insertMarkdown$ } from './plugins/core' import { RealmPlugin, RealmWithPlugins } from './RealmWithPlugins' import { useCellValues, usePublisher, useRealm } from '@mdxeditor/gurx' @@ -121,6 +122,11 @@ export interface MDXEditorMethods { */ setMarkdown: (value: string) => void + /** + * Inserts markdown at the current cursor position. Use the focus if necessary. + */ + insertMarkdown: (value: string) => void + /** * Sets focus on input */ @@ -182,6 +188,9 @@ const Methods: React.FC<{ mdxRef: React.ForwardedRef }> = ({ m setMarkdown: (markdown) => { realm.pub(setMarkdown$, markdown) }, + insertMarkdown: (markdown) => { + realm.pub(insertMarkdown$, markdown) + }, focus: (callbackFn?: (() => void) | undefined, opts?: { defaultSelection?: 'rootStart' | 'rootEnd'; preventScroll?: boolean }) => { realm.getValue(rootEditor$)?.focus(callbackFn, opts) } diff --git a/src/examples/insert-markdown.tsx b/src/examples/insert-markdown.tsx new file mode 100644 index 00000000..c2bbc0a5 --- /dev/null +++ b/src/examples/insert-markdown.tsx @@ -0,0 +1,188 @@ +import React from 'react' +import { + DiffSourceToggleWrapper, + GenericJsxEditor, + JsxComponentDescriptor, + MDXEditor, + MDXEditorMethods, + diffSourcePlugin, + headingsPlugin, + insertMarkdown$, + jsxPlugin, + listsPlugin, + tablePlugin, + toolbarPlugin, + usePublisher +} from '..' + +const initialMarkdownContent = ` + # Hello World + + This is a dummy markdown content. +` + +const simpleMarkdownContentToInsert = ` +# Hello World + +This is a dummy markdown content. + +## Hello World 2 + +This is a dummy markdown content heading 2. +` + +const complexMarkdownContentToInsert = ` +### List + +* hello +* world + * indented + * more +* back + +1. more +2. more + +* [x] Walk the dog +* [ ] Watch movie +* [ ] Have dinner with family + +... an all empty list + +* [ ] Walk the dog +* [ ] Watch movie +* [ ] Have dinner with family + +### Table + +| Syntax | Description | Profit | +| ----------- | ------------- | ------:| +| Header | Title | 50 | +| Paragraph | Text *italic* | 70 | +` + +const jsxMarkdownContentToInsert = ` +import { BlockNode } from './external'; + + + Content *foo* + + more Content + +` + +export function InsertSimpleMarkdown() { + return ( + <> + ( + + + + ) + }) + ]} + onChange={(md) => { + console.log('change', md) + }} + /> + + ) +} + +const InsertSimpleMarkdownButton = () => { + const insertMarkdown = usePublisher(insertMarkdown$) + + return ( + <> + + + ) +} + +export function InsertMarkdownWithTableAndList() { + return ( + <> + ( + + + + ) + }) + ]} + onChange={(md) => { + console.log('change', md) + }} + /> + + ) +} + +const InsertComplexMarkdownButton = () => { + const insertMarkdown = usePublisher(insertMarkdown$) + + return ( + <> + + + ) +} + +const jsxComponentDescriptors: JsxComponentDescriptor[] = [ + { + name: 'BlockNode', + kind: 'flow', + source: './external', + props: [], + hasChildren: true, + Editor: GenericJsxEditor + } +] + +export function InsertMarkdownToNestedEditor() { + const ref = React.useRef(null) + return ( + <> + + + + + + ) +} diff --git a/src/importMarkdownToLexical.ts b/src/importMarkdownToLexical.ts index 4829d8e5..8e6a4ed5 100644 --- a/src/importMarkdownToLexical.ts +++ b/src/importMarkdownToLexical.ts @@ -74,12 +74,17 @@ function isParent(node: unknown): node is Mdast.Parent { return (node as { children?: any[] }).children instanceof Array } +export interface ImportPoint { + append(node: LexicalNode): void + getType(): string +} + /** * The options of the tree import utility. Not meant to be used directly. * @internal */ export interface MdastTreeImportOptions { - root: LexicalRootNode + root: ImportPoint visitors: MdastImportVisitor[] mdastRoot: Mdast.Root } @@ -204,5 +209,5 @@ export function importMdastTreeToLexical({ root, mdastRoot, visitors }: MdastTre }) } - visit(mdastRoot, root, null) + visit(mdastRoot, root as unknown as LexicalNode, null) } diff --git a/src/plugins/core/index.ts b/src/plugins/core/index.ts index fde70c4b..7aaa13e0 100644 --- a/src/plugins/core/index.ts +++ b/src/plugins/core/index.ts @@ -39,6 +39,7 @@ import { mdxMd } from 'micromark-extension-mdx-md' import React from 'react' import { LexicalConvertOptions, exportMarkdownFromLexical } from '../../exportMarkdownFromLexical' import { + ImportPoint, MarkdownParseError, MarkdownParseOptions, MdastImportVisitor, @@ -292,7 +293,7 @@ export const setMarkdown$ = Signal((r) => { ([theNewMarkdownValue, , editor, inFocus]) => { editor?.update(() => { $getRoot().clear() - tryImportingMarkdown(r, theNewMarkdownValue) + tryImportingMarkdown(r, $getRoot(), theNewMarkdownValue) if (!inFocus) { $setSelection(null) @@ -304,6 +305,38 @@ export const setMarkdown$ = Signal((r) => { ) }) +/** + * Inserts new markdown value into the current cursor position of the active editor. + * @group Core + */ +export const insertMarkdown$ = Signal((r) => { + r.sub(r.pipe(insertMarkdown$, withLatestFrom(activeEditor$, inFocus$)), ([markdownToInsert, editor, inFocus]) => { + editor?.update(() => { + const selection = $getSelection() + if (selection !== null) { + const importPoint = { + children: [] as LexicalNode[], + append(node: LexicalNode) { + this.children.push(node) + }, + getType() { + return selection?.getNodes()[0].getType() + } + } + + tryImportingMarkdown(r, importPoint, markdownToInsert) + $insertNodes(importPoint.children) + } + + if (!inFocus) { + $setSelection(null) + } else { + editor.focus() + } + }) + }) +}) + function rebind() { return scan((teardowns, [subs, activeEditorValue]: [EditorSubscription[], LexicalEditor | null]) => { teardowns.forEach((teardown) => { @@ -531,13 +564,13 @@ export const createActiveEditorSubscription$ = Appender(activeEditorSubscription ]) }) -function tryImportingMarkdown(r: Realm, markdownValue: string) { +function tryImportingMarkdown(r: Realm, node: ImportPoint, markdownValue: string) { try { //////////////////////// // Import initial value //////////////////////// importMarkdownToLexical({ - root: $getRoot(), + root: node, visitors: r.getValue(importVisitors$), mdastExtensions: r.getValue(mdastExtensions$), markdown: markdownValue, @@ -566,7 +599,7 @@ export const initialRootEditorState$ = Cell(null, r.pub(rootEditor$, theRootEditor) r.pub(activeEditor$, theRootEditor) - tryImportingMarkdown(r, r.getValue(initialMarkdown$)) + tryImportingMarkdown(r, $getRoot(), r.getValue(initialMarkdown$)) const autoFocusValue = r.getValue(autoFocus$) if (autoFocusValue) {