English | 中文
Markdown editor for react
, developed in jsx
and typescript
.
Documentation and demo:Go
Use it online:Go
The same series editor for vue3:md-editor-v3
prettier
(only for markdown content, not the code and other text).default
, vuepress
, github
, cyanosis
, mk-cute
, smart-blue
styles(not identical). It can be customized also(Refer to example page).mermaid
(>=1.3.0), katex
mathematical formula(>=1.4.0).yarn add md-editor-rt
Use existing extension of language and theme, such as Japanese
yarn add @vavt/cm-extension
Use existing components of toolbar, such as exporting content as PDF
yarn add @vavt/v3-extension
For more ways to use or contribute, please refer to: md-editor-extension
When using server-side rendering, make sure to set editorId
to a constant value.
Starting from 4.0.0
, internal components can be imported on-demand.
import React, { useState } from 'react';
import { MdEditor } from 'md-editor-rt';
import 'md-editor-rt/lib/style.css';
export default () => {
const [text, setText] = useState('# Hello Editor');
return <MdEditor modelValue={text} onChange={setText} />;
};
import React, { useState } from 'react';
import { MdPreview, MdCatalog } from 'md-editor-rt';
import 'md-editor-rt/lib/preview.css';
const scrollElement = document.documentElement;
export default () => {
const [text] = useState('# Hello Editor');
const [id] = useState('preview-only');
return (
<>
<MdPreview editorId={id} modelValue={text} />
<MdCatalog editorId={id} scrollElement={scrollElement} />
</>
);
};
When using server-side rendering, scrollElement
should be of string type, eg: body
, #id
, .class
.
Default theme | Dark theme | Preview only |
---|---|---|
Inputing prompt and mark, emoji extensions
name | type | default | description |
---|---|---|---|
modelValue | string |
'' | Markdown content |
theme | 'light' \| 'dark' |
'light' | Editor theme |
className | string |
'' | |
style | CSSProperties |
{} | Editor inline style |
language | string |
'zh-CN' | Build-in language('zh-CN','en-US') |
editorId | string |
'md-editor-rt_[\d]' | Editor's id, default incrementing by number. When using server-side rendering, make sure to set this attribute to a constant value |
showCodeRowNumber | boolean |
true | Show row number for code block or not |
previewTheme | 'default' \| 'github' \| 'vuepress' \| 'mk-cute' \| 'smart-blue' \| 'cyanosis' |
'default' | Preview theme, can be customized |
noMermaid | boolean |
false | Use mermaid or not |
noKatex | boolean |
false | Use katex or not |
codeTheme | 'atom' \| 'a11y' \| 'github' \| 'gradient' \| 'kimbie' \| 'paraiso' \| 'qtcreator' \| 'stackoverflow' |
'atom' | Highlight code style, can be customized also |
mdHeadingId | (text: string, level: number, index: number) => string |
(text) => text | H1-H6 ID generator |
sanitize | (html: string) => string |
(html) => html | This attribute is used to alter the compiled HTML content |
noIconfont | boolean |
false | Not append iconfont script, you can get the latest link here |
formatCopiedText | (text: string) => string |
(text: string) => text | Format copied code |
codeStyleReverse | boolean |
true | Code style will be reversed to dark while code block of the theme has a dark background |
codeStyleReverseList | Array<string> |
['default', 'mk-cute'] | Themes to be reversed |
noHighlight | boolean |
false | Highlight code or not |
noImgZoomIn | boolean |
false | Enable the function of enlarging images |
customIcon | CustomIcon |
{} | Customized icons |
sanitizeMermaid | (h: string) => Promise<string> |
(h: string) => Promise.resolve(h) | Convert the generated mermaid code |
codeFoldable | boolean |
true | Whether to enable code folding feature |
autoFoldThreshold | number |
30 | Threshold for triggering automatic code folding by line count |
Except for the same as MdPreview
:
name | type | default | description |
---|---|---|---|
pageFullscreen | boolean |
false | Screenfull in web page |
preview | boolean |
true | Preview content in editor |
htmlPreview | boolean |
false | Preview html in editor(If true, preview must be false) |
toolbars | Array<ToolbarNames \| number> |
[toolbars] | Show contents of toolbar, all keyssee toolbars below |
toolbarsExclude | Array<ToolbarNames \| number> |
[] | Don't show contents of toolbar, all keystoolbars |
noPrettier | boolean |
false | Use prettier to beautify content or not |
tabWidth | number |
2 | One tab eq some spaces |
tableShape | [number, number] \| [number, number, number, number] |
[6, 4] | Preset the size of the table, [columns, rows, Maximum number of columns, Maximum number of rows] |
placeholder | string |
'' | |
defToolbars | Array<DropdownToolbar \| NormalToolbar \| ModalToolbar> |
[] | Custom toolbar in DropdownToolbar , NormalToolbar or ModalToolbar |
footers | Array<'markdownTotal' \| '=' \| 'scrollSwitch' \| number> |
['markdownTotal', '=', 'scrollSwitch'] | Show contents of footer, they are divided by '=' . Set it to [] to hidden footer |
scrollAuto | boolean |
true | Scroll default setting |
defFooters | Array<string \| ReactElement> |
[] | Custom footer |
noUploadImg | boolean |
false | Not show the entrance to upload pictures |
autoFocus | boolean |
false | same as autofocus in native textarea |
disabled | boolean |
false | same as disabled in native textarea |
readOnly | boolean |
false | same as readonly in native textarea |
maxLength | number |
same as maxlength in native textarea |
|
autoDetectCode | boolean |
false | auto detect the type of pasted code, only support that copied from vscode |
completions | Array<CompletionSource> |
[] | @codemirror/autocomplete List of function to match keywords |
showToolbarName | boolean |
false | Show toolbar name or not |
inputBoxWitdh | string |
'50%' | Default width of input box |
transformImgUrl | (imgUrl: string) => string \| Promise<string> |
t => t | Transform image links |
You can sort the toolbar as you like, split tools by
'-'
, the left and right toolbars are divided by'='
!
name | type | description |
---|---|---|
onChange | value: string |
Content changed(bind to oninput of textarea ) |
onHtmlChanged | html: string |
Compile markdown successful event, you can use it to get the html code |
onGetCatalog | list: Array<HeadList> |
Get catalog of article |
Except for the same as MdPreview
:
name | type | description |
---|---|---|
onSave | value: string, html: Promise<string> |
Saving content, ctrl+s and clicking button will trigger it |
onUploadImg | files: Array<File>, callback: (urls: string[] \| { url: string; alt: string; title: string }[]) => void |
Uploading picture, when picture is uploading the modal will not close, please provide right urls to the callback function |
onError | err: { name: 'Cropper' \| 'fullscreen' \| 'prettier' \| 'overlength'; message: string } |
Catch run-time error, Cropper , fullscreen and prettier are used when they are not loaded. And content exceeds the length limit error |
onBlur | event: FocusEvent<HTMLTextAreaElement, Element> |
Textarea has lost focus |
onFocus | event: FocusEvent<HTMLTextAreaElement, Element> |
Textarea has received focus |
onInput | event: Event |
Element gets input |
onDrop | event: DragEvent |
Selection is being dragged |
onInputBoxWitdhChange | (width: string) => void |
Width of input box has been changed |
After 2.5.0, Editor exposes several methods on the instance, used to get or change the internal status of the editor.
import React, { useState, useEffect, useRef } from 'react';
import { MdEditor, ExposeParam } from 'md-editor-rt';
import 'md-editor-rt/lib/style.css';
export default () => {
const [text, setText] = useState('#Hello Editor');
const editorRef = useRef<ExposeParam>();
useEffect(() => {
editorRef.current?.on('catalog', console.log);
}, []);
return <MdEditor ref={editorRef} modelValue={text} onChange={setText} />;
};
Switched to the opposite status, if toggle without input parameter.
Get the internal state of the editor, including pageFullscreen, fullscreen, preview, htmlPreview, catalog, etc.
type Keys =
| 'pageFullscreen'
| 'fullscreen'
| 'preview'
| 'previewOnly'
| 'htmlPreview'
| 'catalog';
editorRef.current?.on('pageFullscreen', (status) => console.log(status));
Toggle status of fullscreen within the page.
editorRef.current?.togglePageFullscreen(true);
Toggle status of fullscreen widthin browser.
editorRef.current?.toggleFullscreen(true);
Toggle status of preview.
editorRef.current?.togglePreview(true);
Toggle into Preview Only Mode
editorRef.current?.togglePreviewOnly(true);
Toggle status of htmlPreview.
editorRef.current?.toggleHtmlPreview(true);
Toggle status of catalog.
editorRef.current?.toggleCatalog(true);
Save actions will be triggered.
Manually insert content into textarea.
/**
* @params selectedText
*/
editorRef.current?.insert((selectedText) => {
/**
* @return targetValue Content to be inserted
* @return select Automatically select content, default: true
* @return deviationStart Start position of the selected content, default: 0
* @return deviationEnd End position of the selected content, default: 0
*/
return {
targetValue: `${selectedText}`,
select: true,
deviationStart: 0,
deviationEnd: 0
};
});
For more examples, refer to source code of extension component
Focus on textarea.
import type { FocusOption } from 'md-editor-rt';
const option: FocusOption | undefined = 'start';
// Cursor position when focusing on textarea, default: position when it last lost focus
editorRef.current?.focus(option);
Re render the content.
Get the currently selected text.
Clear current history.
Supports listening to all DOM events.
editorRef.current?.domEventHandlers({
compositionstart: () => {
console.log('compositionstart');
}
});
Insert content into the editor via trigger.
editorRef.current?.execCommand('bold');
Get codemirror instance.
Use config(option: ConfigOption)
to reconfigure markdown-it
and so on.
[!WARNING]
We recommend configuring it at the project entry point, such as in
main.js
for projects created with Vite. Avoid callingconfig
within components!
Customize new extensions based on theme and default extensions f codeMirror.
Example: Editor does not render the line number of textarea by default, this extension needs to be manually added
import { config } from 'md-editor-rt';
import { lineNumbers } from '@codemirror/view';
config({
codeMirrorExtensions(_theme, extensions) {
return [...extensions, lineNumbers()];
}
});
Customize extensions, attributes of markdown-it
, etc.
type MarkdownItConfig = (
md: markdownit,
options: {
editorId: string;
}
) => void;
Example: Use markdown-it-anchor
to render a hyperlink symbol to the right of the title
import { config } from 'md-editor-rt';
import ancher from 'markdown-it-anchor';
config({
markdownItConfig(mdit) {
mdit.use(ancher, {
permalink: true
});
}
});
Select and add built-in plugins to markdown-it
.
type MarkdownItPlugins = (
plugins: Array<MarkdownItConfigPlugin>,
options: {
editorId: string;
}
) => Array<MarkdownItConfigPlugin>;
Example: Modify the class name of the image.
import { config } from 'md-editor-rt';
config({
markdownItPlugins(plugins) {
return plugins.map((p) => {
if (p.type === 'image') {
return {
...p,
options: {
...p.options,
classes: 'my-class'
}
};
}
return p;
});
}
});
Add more languages, reset mermaid
template or delay rendering time:
import { config } from 'md-editor-rt';
config({
editorConfig: {
languageUserDefined: { lang: StaticTextDefaultValue },
mermaidTemplate: {
flow: `flow tempalte`,
...more
},
// Default 500ms. It is set to 0ms when preview only and not set.
renderDelay: 500,
// for modal component
zIndex: 2000
}
});
Config some dependency libraries, like highlight..
import { config } from 'md-editor-rt';
config({
editorExtensions: { iconfont: 'https://xxx.cc' }
});
Synchronously add attributes to the CDN link tags, consistent with the type of editorExtensions
, with a value type of HTMLElementTagNameMap['tagName']
.
import { config, editorExtensionsAttrs } from 'md-editor-rt';
config({
editorExtensionsAttrs
});
Do not attempt to define the src \ onload \ id of the script and rel \ href \ id of the link in editorExtensionsAttrs, as they will be overwritten by default values
Set the way to display icons:
svg
: with symbolclass
: with font-classIf the icon is customized through the attribute customIcon
, the customized icon will be used first.
Configure mermaid
, Details
import { config } from 'md-editor-rt';
config({
mermaidConfig(base: any) {
return {
...base,
logLevel: 'error'
};
}
});
Configure katex
, Details
import { config } from 'md-editor-rt';
config({
katexConfig(base: any) {
return {
...base,
strict: false
};
}
});
Pay attention: shortcut keys are only available when the textarea has received focus!
key | function | description |
---|---|---|
TAB | insert space | Insert space, the length eq tabWidth , default: 2, support multiline |
SHIFT + TAB | delete space, setting is the same as Tab | |
CTRL + C | copy | When selected, copy the selected content. When not selected, copy the content of the current line |
CTRL + X | shear | When selected, cut the selected content. When not selected, cut the current line |
CTRL + D | delete | When selected, delete the selected content. When not selected, delete the current line |
CTRL + S | save | Trigger onSave event |
CTRL + B | bold text | **bold** |
CTRL + U | underline | <u>underline</u> |
CTRL + I | italic | *italic* |
CTRL + 1-6 | h1-h6 | # title |
CTRL + ↑ | superscript | <sup>superscript</sup> |
CTRL + ↓ | subscript | <sub>subscript</sub> |
CTRL + O | ordered list | 1. ordered list |
CTRL + L | link | [link](https://github.com/imzbf/md-editor-rt) |
CTRL + Z | withdraw | Withdraw history in editor, not the function of system |
CTRL + SHIFT + S | line-through | ~line-through~ |
CTRL + SHIFT + U | unordered list | - unordered list |
CTRL + SHIFT + C | code block | |
CTRL + SHIFT + I | picture | ![picture](https://imzbf.github.io/md-editor-rt/imgs/preview-light.png) |
CTRL + SHIFT + Z | forward | Forward history in editor, not the function of system |
CTRL + SHIFT + F | Beautify | |
CTRL + ALT + C | code row | |
CTRL + SHIFT + ALT + T | table | \|table\| |
import { NormalToolbar } from 'md-editor-rt';
For more examples, refer to document.
NormalToolbar
props
title
: string
, not necessary, title of toolbar.trigger
: ReactNode
, necessary, it is usually an icon, which is displayed on the toolbar.events
onClick
: (e: MouseEvent) => void
, necessary.DropdownToolbar
props
title
: string
, not necessary, title of toolbar.visible
: boolean
, necessary.trigger
: ReactNode
, necessary, it is usually an icon, which is displayed on the toolbar.overlay
: ReactNode
, necessary, content of dropdown box.events
onChange
: (visible: boolean) => void
, necessary.ModalToolbar
props
title
: string
, not necessary, title of toolbar.modalTitle
: ReactNode
, not necessary, title of the Modal.visible
: boolean
, necessary, visibility of Modal.width
: string
, not necessary, width of Modal, default auto
.height
: string
, same as width
.showAdjust
: boolean
, not necessary, visibility of fullscreen button.isFullscreen
: boolean
, necessary when showAdjust = true
, status of fullscreen.trigger
: ReactNode
, necessary, it is usually an icon, which is displayed on the toolbar.children
: ReactNode
, necessary, content of Modal.className
: string
, not necessary.style
: CSSProperties
, not necessary.showMask
: boolean
, not necessary, whether to display the mask layer, default true
.events
onClick
: () => void
, necessary.onClose
: () => void
, necessary, closed event.onAdjust
: (val: boolean) => void
, fullscreen button was clicked.MdCatalog
props
editorId
: string
, necessary, same as editor's editorId
, used to register listening events.className
: string
, not necessary.mdHeadingId
: MdHeadingId
, not necessary, same as editor.scrollElement
: string | HTMLElement
, not necessary, it is an element selector when its type is string. When previewOnly
eq true
, it is usually set to document.documentElement
.theme
: 'light' | 'dark'
, not necessary, provide it when you want to change theme online, it is the same as Editor theme
.offsetTop
: number
, not necessary, highlight current item of catalogs when title is offsetTop
pixels from the top, default 20.scrollElementOffsetTop
: number
, not necessary, offsetTop of the scroll container,default 0.isScrollElementInShadow
: boolean
, whether the scroll container is in web component, default false.events
onClick
: (e: MouseEvent, t: TocItem) => void
, not necessary, heading was clicked.onActive
: (heading: HeadList | undefined) => void
, not necessary, heading was highlighted.MdModal
props
title
: ReactNode
, not necessary, title of Modal.visible
: boolean
, necessary, visibility of Modal.width
: string
, not necessary, width of Modal, default auto
.height
: string
, same as width
.showAdjust
: boolean
, not necessary, visibility of fullscreen button.isFullscreen
: boolean
, necessary when showAdjust = true
, status of fullscreen.children
: ReactNode
, necessary, content of Modal.className
: string
, not necessary.style
: CSSProperties
, not necessary.showMask
: boolean
, not necessary, whether to display the mask layer, default true
.events
onClose
: () => void
, necessary, closed event.onAdjust
: (val: boolean) => void
, fullscreen button was clicked.import {
iconfontClassUrl,
iconfontSvgUrl,
allToolbar,
allFooter,
zh_CN,
en_US
} from 'md-editor-rt';
console.log(iconfontClassUrl, iconfontSvgUrl, allToolbar, allFooter, zh_CN, en_US);
Tips: When you paste and upload GIF, it will upload a static picture. So you should upload it by file system!
import React, { useState } from 'react';
import { MdEditor } from 'md-editor-rt';
import 'md-editor-rt/lib/style.css';
export default () => {
const [text, setText] = useState('# Hello Editor');
const onUploadImg = async (files, callback) => {
const res = await Promise.all(
files.map((file) => {
return new Promise((rev, rej) => {
const form = new FormData();
form.append('file', file);
axios
.post('/api/img/upload', form, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then((res) => rev(res))
.catch((error) => rej(error));
});
})
);
// Approach 1
callback(res.map((item) => item.data.url));
// Approach 2
// callback(
// res.map((item: any) => ({
// url: item.data.url,
// alt: 'alt',
// title: 'title'
// }))
// );
};
return <MdEditor modelValue={text} onChange={setText} onUploadImg={onUploadImg} />;
};
.css-vars(@isDark) {
--md-color: if(@isDark, #999, #222);
--md-hover-color: if(@isDark, #bbb, #000);
--md-bk-color: if(@isDark, #000, #fff);
--md-bk-color-outstand: if(@isDark, #333, #f2f2f2);
--md-bk-hover-color: if(@isDark, #1b1a1a, #f5f7fa);
--md-border-color: if(@isDark, #2d2d2d, #e6e6e6);
--md-border-hover-color: if(@isDark, #636262, #b9b9b9);
--md-border-active-color: if(@isDark, #777, #999);
--md-modal-mask: #00000073;
--md-modal-shadow: if(@isDark, 0px 6px 24px 2px #00000066, 0px 6px 24px 2px #00000019);
--md-scrollbar-bg-color: if(@isDark, #0f0f0f, #e2e2e2);
--md-scrollbar-thumb-color: if(@isDark, #2d2d2d, #0000004d);
--md-scrollbar-thumb-hover-color: if(@isDark, #3a3a3a, #00000059);
--md-scrollbar-thumb-active-color: if(@isDark, #3a3a3a, #00000061);
}
.md-editor {
.css-vars(false);
}
.md-editor-dark {
.css-vars(true);
}
Change background color in dark mode:
.md-editor-dark {
--md-bk-color: #333 !important;
}