Easy-to-use Markdown editor, built to adapt to different application scenarios
English | Demo
Vditor is a browser-side Markdown editor that supports WYSIWYG, instant rendering (similar to Typora) and split-screen preview modes. It is implemented using TypeScript and supports native JavaScript as well as frameworks such as Vue, React, Angular and Svelte.
Welcome to the Vditor official discussion area to learn more. At the same time, we also welcome to follow B3log Open Source Community WeChat official account B3log开源:
With the popularity of Markdown typesetting methods, more and more applications have begun to integrate Markdown editors. The current status of mainstream integrated Markdown editors is as follows:
These three points correspond exactly to three application scenarios:
Therefore, a Markdown editor that can adapt to the application scenario is crucial, and it needs to be considered:
Vditor has made efforts in these areas, hoping to make some contribution to the modern universal Markdown editing field.
The WYSIWYG mode is more friendly to users who are not familiar with Markdown, and can also be used seamlessly if you are familiar with Markdown.
Instant rendering mode should not be unfamiliar to users who are familiar with Typora. In theory, this is the most elegant way to edit Markdown.
The traditional split-screen preview mode is suitable for Markdown editing under large screens.
Most of the above features can be enabled by switching configuration. Developers can choose matching based on their application scenarios.
npm install vditor --save import Vditor from 'vditor'
import "~vditor/src/assets/less/index"
const vditor = new Vditor ( id , { options... } ) <!-- ️生产环境请指定版本号,如 https://unpkg.com/[email protected]/dist... -->
< link rel =" stylesheet " href =" https://unpkg.com/vditor/dist/index.css " />
< script src =" https://unpkg.com/vditor/dist/index.min.js " > </ script >The appearance of the editor. Built-in classic, dark 2 sets of themes.
options.themesetThemeThe appearance of the HTML output from Markdown. Built-in ant-design, light, dark, wechat 4 sets of themes. Supports content theme extension interface.
class="vditor-reset" to the display elementoptions.preview.themeIPreviewOptions.themesetTheme or setContentTheme The appearance of the code block. Built-in github and other 36 sets of themes.
options.preview.hljsIPreviewOptions.hljssetTheme or setCodeTheme You can fill in the element id or the element's own HTMLElement
HTMLElement of the element itself, you need to set options.cache.id or set options.cache.enable to false
| illustrate | default value | |
|---|---|---|
| i18n | Multilingual, see ITips | - |
| undoDelay | History interval | - |
| After | Callback method after the editor is rendered asynchronously | - |
| height | Total editor height | 'auto' |
| minHeight | Minimum height of edit area | - |
| width | Total editor width, support % | 'auto' |
| placeholder | Prompt when the input area is empty | '' |
| lang | Language types: en_US, fr_FR, pt_BR, ja_JP, ko_KR, ru_RU, sv_SE, zh_CN, zh_TW | 'zh_CN' |
| input(value: string) | Triggered after input | - |
| focus(value: string) | Trigger after focus | - |
| blur(value: string) | Triggered after out of focus | - |
| keydown(event: KeyboardEvent) | Triggered after pressing | - |
| esc(value: string) | Triggered after esc press | - |
| ctrlEnter(value: string) | ⌘/ctrl+enter is triggered after pressing | - |
| select(value: string) | Triggered after selecting text in the editor | - |
| tab | Tab key operates strings, supports t and any strings | - |
| typewriterMode | Whether to enable typewriter mode | false |
| cdn | Configure a self-built CDN address | https://unpkg.com/vditor@${VDITOR_VERSION} |
| mode | Optional mode: sv, ir, wysiwyg | 'ir' |
| debugger | Whether to display logs | false |
| value | Editor initialization value | '' |
| theme | Topic: classic, dark | 'classic' |
| icon | Icon style: ant, material | 'ant' |
| customRenders: {language: string, render: (element: HTMLElement, vditor: IVditor) => void}[] | Custom renderer | [] |
toolbar: ['emoji', 'br', 'bold', '|', 'line'] . See src/ts/util/Options.ts for the default valueemoji , headings , bold , italic , strike , | , line , quote , list , ordered-list , check , outdent , indent , code , inline-code , insert-after , insert-before , undo , redo , upload , link , table , record , edit-mode , both , preview , fullscreen , outline , code-theme , content-theme , export , devtools , info , help , brname is not in the enumeration, you can add a custom button, in the format as follows: new Vditor ( 'vditor' , {
toolbar : [
{
hotkey : '⇧⌘S' ,
name : 'sponsor' ,
tipPosition : 's' ,
tip : '成为赞助者' ,
className : 'right' ,
icon : '<svg t="1589994565028" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2808" width="32" height="32"><path d="M506.6 423.6m-29.8 0a29.8 29.8 0 1 0 59.6 0 29.8 29.8 0 1 0-59.6 0Z" fill="#0F0F0F" p-id="2809"></path><path d="M717.8 114.5c-83.5 0-158.4 65.4-211.2 122-52.7-56.6-127.7-122-211.2-122-159.5 0-273.9 129.3-273.9 288.9C21.5 562.9 429.3 913 506.6 913s485.1-350.1 485.1-509.7c0.1-159.5-114.4-288.8-273.9-288.8z" fill="#FAFCFB" p-id="2810"></path><path d="M506.6 926c-22 0-61-20.1-116-59.6-51.5-37-109.9-86.4-164.6-139-65.4-63-217.5-220.6-217.5-324 0-81.4 28.6-157.1 80.6-213.1 53.2-57.2 126.4-88.8 206.3-88.8 40 0 81.8 14.1 124.2 41.9 28.1 18.4 56.6 42.8 86.9 74.2 30.3-31.5 58.9-55.8 86.9-74.2 42.5-27.8 84.3-41.9 124.2-41.9 79.9 0 153.2 31.5 206.3 88.8 52 56 80.6 131.7 80.6 213.1 0 103.4-152.1 261-217.5 324-54.6 52.6-113.1 102-164.6 139-54.8 39.5-93.8 59.6-115.8 59.6zM295.4 127.5c-72.6 0-139.1 28.6-187.3 80.4-47.5 51.2-73.7 120.6-73.7 195.4 0 64.8 78.3 178.9 209.6 305.3 53.8 51.8 111.2 100.3 161.7 136.6 56.1 40.4 88.9 54.8 100.9 54.8s44.7-14.4 100.9-54.8c50.5-36.3 108-84.9 161.7-136.6 131.2-126.4 209.6-240.5 209.6-305.3 0-74.9-26.2-144.2-73.7-195.4-48.2-51.9-114.7-80.4-187.3-80.4-61.8 0-127.8 38.5-201.7 117.9-2.5 2.6-5.9 4.1-9.5 4.1s-7.1-1.5-9.5-4.1C423.2 166 357.2 127.5 295.4 127.5z" fill="#141414" p-id="2811"></path><path d="M353.9 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2812"></path><path d="M659.3 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2813"></path><path d="M411.6 538.5c0 52.3 42.8 95 95 95 52.3 0 95-42.8 95-95v-31.7h-190v31.7z" fill="#5B5143" p-id="2814"></path><path d="M506.6 646.5c-59.6 0-108-48.5-108-108v-31.7c0-7.2 5.8-13 13-13h190.1c7.2 0 13 5.8 13 13v31.7c0 59.5-48.5 108-108.1 108z m-82-126.7v18.7c0 45.2 36.8 82 82 82s82-36.8 82-82v-18.7h-164z" fill="#141414" p-id="2815"></path><path d="M450.4 578.9a54.7 27.5 0 1 0 109.4 0 54.7 27.5 0 1 0-109.4 0Z" fill="#EA64F9" p-id="2816"></path><path d="M256 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2817"></path><path d="M703.3 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2818"></path></svg>' ,
click ( ) { alert ( '捐赠地址:https://ld246.com/sponsor' ) } ,
} ] ,
} )| illustrate | default value | |
|---|---|---|
| name | Unique mark | - |
| icon | svg icon | - |
| tip | hint | - |
| tipPosition | Tips: 'n', 'ne', 'nw', 's', 'se', 'sw', 'w', 'e' | - |
| hotkey | Shortcut keys in format ⇧⌘ / ⌘ / ⌥⌘ | - |
| suffix | Insert the suffix in the editor | - |
| prefix | Insert the prefix in the editor | - |
| click(event: Event, vditor: IVditor) | The event that is triggered when a custom button is clicked | - |
| className | Style name | '' |
| toolbar?: Array<options.toolbar> | Submenu | - |
| illustrate | default value | |
|---|---|---|
| Hide | Whether to hide the toolbar | false |
| pin | Whether to fix the toolbar | false |
| illustrate | default value | |
|---|---|---|
| enable | Whether to enable the counter | false |
| after(length: number, counter: options.counter): void | Word count callback | - |
| max | The maximum value allowed to enter | - |
| type | Statistics type: 'markdown', 'text' | 'markdown' |
| illustrate | default value | |
|---|---|---|
| enable | Whether to use localStorage for cache | true |
| id | Cache key, the first parameter is an element and requires cache activation | - |
| after(html: string): string | Cache callback | - |
| illustrate | default value | |
|---|---|---|
| enable | Whether to enable comment mode | false |
| add(id: string, text: string, commentsData: ICommentsData[]) | Add a comment callback | - |
| remove(ids: string[]) | Delete comment callback | - |
| scroll(top: number) | Scroll callback | - |
| adjustTop(commentsData: ICommentsData[]) | When modifying the document, the height of the comments is adapted | - |
| illustrate | default value | |
|---|---|---|
| delay | Preview debounce millisecond intervals | 1000 |
| maxWidth | Maximum width of preview area | 800 |
| mode | Display mode: both, editor | 'both' |
| url | md parsing request | - |
| parse(element: HTMLElement) | Preview callback | - |
| transform(html: string): string | Callback before rendering | - |
| illustrate | default value | |
|---|---|---|
| defaultLang | Use the language by default if no language is specified | '' |
| enable | Whether to enable code highlighting | true |
| style | See Chroma for optional values | github |
| lineNumber | Whether to enable line number | false |
| langs | Customize the specified language | CODE_LANGUAGES |
| renderMenu(code: HTMLElement, copy: HTMLElement) | Render menu button | - |
| illustrate | default value | |
|---|---|---|
| autoSpace | Automatic spaces | false |
| gfmAutoLink | Automatic link | true |
| fixTermTypo | Automatic correction term | false |
| toc | Insert directory | false |
| footnotes | footnote | true |
| codeBlockPreview | Whether to render the code block in wysiwyg and ir modes | true |
| mathBlockPreview | Whether to render mathematical formulas in wysiwyg and ir modes | true |
| paragraphBeginningSpace | Two empty beginnings | false |
| sanitize | Whether to enable XSS filtering | true |
| listStyle | Add data-style attribute to the list | false |
| linkBase | Link relative path prefix | '' |
| linkPrefix | Link mandatory prefix | '' |
| mark | Enable mark mark | false |
| illustrate | default value | |
|---|---|---|
| Current | Current topic | "light" |
| list | List of optional topics | { "ant-design": "Ant Design", dark: "Dark", light: "Light", wechat: "WeChat" } |
| path | Theme style address | https://unpkg.com/vditor@${VDITOR_VERSION}/dist/css/content-theme |
| illustrate | default value | |
|---|---|---|
| inlineDigit | Is it allowed to count after the inline mathematical formula starts $? | false |
| macros | Macro definitions passed in when rendering with MathJax | {} |
| engine | Mathematical formula rendering engine: KaTeX, MathJax | 'KaTeX' |
| mathJaxOptions | Parameters when the mathematical formula rendering engine is MathJax | - |
The default values are ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"]. You can select from the default values for configuration, or use the following fields for custom development.
| illustrate | default value | |
|---|---|---|
| key | The button is uniquely identified and cannot be empty | - |
| text | Button text | - |
| tooltip | hint | - |
| className | Button class name | - |
| click(key: string) | Button click callback event | - |
| illustrate | default value | |
|---|---|---|
| enable | Whether to enable multimedia rendering | true |
| illustrate | default value | |
|---|---|---|
| isPreview | Whether to preview the picture | true |
| preview(bom: Element) => void | Image preview processing | - |
| illustrate | default value | |
|---|---|---|
| isOpen | Whether to open the link address | true |
| click(bom: Element) => void | Click link event | - |
| illustrate | default value | |
|---|---|---|
| parse | Whether to perform md analysis | true |
| delay | Prompt debounce millisecond interval | 200 |
| emoji | Default emoji, can be selected from lute/emoji_map, or can be customized | { '+1': '?', '-1': '?', 'heart': '❤️', 'cold_sweat': '?' } |
| emojiTail | Common expression tips | - |
| emojiPath | Expression image address | https://unpkg.com/vditor@${VDITOR_VERSION}/dist/images/emoji |
| extend: IHintExtend[] | Extension of automatic completion of keywords such as @/tours | [] |
interface IHintData {
html : string ;
value : string ;
}
interface IHintExtend {
key : string ;
hint ? ( value : string ) : IHintData [ ] | Promise < IHintData [ ] > ;
} format can be used for conversion. // POST data
xhr . send ( formData ) ; // formData = FormData.append("file[]", File)
// return data
{
"msg" : "" ,
"code" : 0 ,
"data" : {
"errFiles" : [ 'filename' , 'filename2' ] ,
"succMap" : {
"filename3" : "filepath3" ,
"filename3" : "filepath3"
}
}
}linkToImgUrl can pass the off-site picture address in the clipboard to the server for saving, and its data structure is as follows: // POST data
xhr . send ( JSON . stringify ( { url : src } ) ) ; // src 为站外图片地址
// return data
{
msg : '' ,
code : 0 ,
data : {
originalURL : '' ,
url : ''
}
}success , format , error will not be triggered at the same time. The specific call situation is as follows: if ( xhr . status === 200 ) {
if ( vditor . options . upload . success ) {
vditor . options . upload . success ( editorElement , xhr . responseText ) ;
} else {
let responseText = xhr . responseText ;
if ( vditor . options . upload . format ) {
responseText = vditor . options . upload . format ( files as File [ ] , xhr . responseText ) ;
}
genUploadedLabel ( responseText , vditor ) ;
}
} else {
if ( vditor . options . upload . error ) {
vditor . options . upload . error ( xhr . responseText ) ;
} else {
vditor . tip . show ( xhr . responseText ) ;
}
}| illustrate | default value | |
|---|---|---|
| url | Upload the url, if it is empty, the upload-related event will not be triggered. | '' |
| max | Upload file maximum Byte | 10 * 1024 * 1024 |
| linkToImgUrl | When the clipboard contains the image address, use this url to upload again | '' |
| linkToImgCallback(responseText: string) | Upload image address callback | - |
| linkToImgFormat(responseText: string): string | Format the return value uploaded by the image address | - |
| success(editor: HTMLPreElement, msg: string) | Upload successfully callback | - |
| error(msg: string) | Upload failed callback | - |
| token | CORS upload verification, header is X-Upload-Token | - |
| withCredentials | Cross-site access control | false |
| Headers | Request header settings | - |
| filename(name: string): string | File name security processing | name => name.replace(/W/g, '') |
| accept | File upload type, same as input accept | - |
| validate(files: File[]) => string | boolean | Verify, return true if successful otherwise return error message | - |
| handler(files: File[]) => string | null | Promise | Promise | Custom upload, return error message when an error occurs | - |
| format(files: File[], responseText: string): string | Convert the data returned by the server to meet the built-in data structure | - |
| file(files: File[]): File[] | Promise<File[]> | Process the uploaded file before returning | - |
| setHeaders(): { [key: string]: string } | Use the return value to set the header before uploading | - |
| extraData: { [key: string]: string | Blob } | Add extra parameters to FormData | - |
| multiple | Is there multiple uploaded files | true |
| fieldName | Upload field name | 'file[]' |
| renderLinkDest?(vditor: IVditor, node: ILuteNode, entering: boolean): [string, number] | Process the image address in the clipboard | '' |
| illustrate | default value | |
|---|---|---|
| enable | Is it supported for size dragging? | false |
| position | Drag bar position: 'top', 'bottom' | 'bottom' |
| After(height: number) | Callback ended with dragging | - |
| illustrate | default value | |
|---|---|---|
| preview | className on preview element | '' |
| illustrate | default value | |
|---|---|---|
| index | Full screen level | 90 |
| illustrate | default value | |
|---|---|---|
| enable | Whether the initialization displays the outline | false |
| position | Outline location: 'left', 'right' | 'left' |
| illustrate | |
|---|---|
| exportJSON(markdown: string) | Get the corresponding JSON according to Markdown |
| getValue() | Get Markdown content |
| getHTML() | Get HTML content |
| insertValue(value: string, render = true) | Insert content in focus and use Markdown rendering by default |
| focus() | Focus on the editor |
| blur() | Make the editor out of focus |
| disabled() | Disable the editor |
| enable() | Undisable the editor |
| getSelection(): string | Returns the selected string |
| setValue(markdown: string, clearStack = false) | Set the editor content and select Clear history stack |
| clearStack() | Clear the undo and redo record stack |
| renderPreview(value?: string) | Set preview area content |
| getCursorPosition():{top: number, left: number} | Get focus position |
| deleteValue() | Delete selected content |
| updateValue(value: string) | Update selected content |
| isUploading() | Is the upload still in progress |
| clearCache() | Clear cache |
| disabledCache() | Disable cache |
| enableCache() | Enable cache |
| html2md(value: string) | HTML to md |
| tip(text: string, time: number) | Message prompt. Time is 0 and will be displayed |
| setPreviewMode(mode: "both" | "editor") | Set preview mode |
| setTheme(theme: "dark" | "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) | Set theme, content theme and code block style |
| getCurrentMode(): string | Get the editor's current editing mode |
| destroy() | Destroy the editor |
| getCommentIds(): {id: string, top: number}[] | Get all comments |
| hlCommentIds(ids: string[]) | Highlight Comments |
| unHlCommentIds(ids: string[]) | Cancel comments and highlights |
| removeCommentIds(removeIds: string[]) | Delete comments |
| updateToolbarConfig(config: {hide?: boolean, pin?: boolean}) | Update toolbar configuration |
method.min.js and call it directly as follows Vditor . mermaidRender ( document ) import VditorPreview from 'vditor/dist/method.min'
VditorPreview . mermaidRender ( document )preview method, and the parameters are as follows: previewElement: HTMLDivElement , // 使用该元素进行渲染
markdown : string , // 需要渲染的 markdown 原文
options ?: IPreviewOptions {
mode : "dark" | "light" ;
anchor?: number ; // 为标题添加锚点 0:不渲染;1:渲染于标题前;2:渲染于标题后,默认 0
customEmoji?: { [ key : string ] : string } ; // 自定义 emoji,默认为 {}
lang?: ( keyof II18nLang ) ; // 语言,默认为 'zh_CN'
emojiPath?: string ; // 表情图片路径
hljs?: IHljs ; // 参见 options.preview.hljs
speech?: { // 对选中后的内容进行阅读
enable ?: boolean ,
} ;
math?: IMath ; // 数学公式渲染配置
cdn?: string ; // 自建 CDN 地址
transform ? ( html : string ) : string ; // 在渲染前进行的回调方法
after ? ( ) ; // 渲染完成后的回调
lazyLoadImage?: string ; // 设置为 Loading 图片地址后将启用图片的懒加载
markdown?: options . preview . markdown ;
theme?: options . preview . theme ;
render?: options . preview . render ;
renderers?: ILuteRender ; // 自定义渲染 https://ld246.com/article/1588412297062
}method.min.js and index.min.js cannot be introduced at the same time| illustrate | |
|---|---|
| previewImage(oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") | Click on the image to preview |
| mermaidRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) | Flow chart/time chart/gant chart |
| SMILESRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) | Chemical substance structure |
| markmapRender(element: HTMLElement, cdn = options.cdn) | markdown mind map |
| flowchartRender(element: HTMLElement, cdn = options.cdn) | flowchart rendering |
| codeRender(element: HTMLElement, option?: IHljs) | Add a copy button to the code block in element |
| chartRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) | Chart rendering |
| mindmapRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) | Brain image rendering |
| plantumlRender(element: (HTMLElement | Document) = document, cdn = options.cdn) | plantuml rendering |
| abcRender(element: (HTMLElement | Document) = document, cdn = options.cdn) | Stitch rendering |
| md2html(mdText: string, options?: IPreviewOptions): Promise<string> | Markdown text is converted to HTML, this method requires asynchronous programming |
| preview(previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) | Page Markdown Article Rendering |
| highlightRender(hljsOption?: IHljs, element?: HTMLElement | Document, cdn = options.cdn) | Highlight rendering of code blocks in element |
| mediaRender(element: HTMLElement) | Render as video, audio, and embedded iframes for specific links |
| mathRender(element: HTMLElement, options?: {cdn?: string, math?: IMath}) | Rendering mathematical formulas |
| speechRender(element: HTMLElement, lang?: (keyof II18nLang)) | Read selected text |
| graphvizRender(element: HTMLElement, cdn?: string) | Render graphviz |
| outlineRender(contentElement: HTMLElement, targetElement: Element) | Render the outline |
| lazyLoadImageRender(element: (HTMLElement | Document) = document) | Rendering pictures with lazy loading enabled |
| setCodeTheme(codeTheme: string, cdn = options.cdn) | Set the code theme, see options.preview.hljs.style for codeTheme |
| setContentTheme(contentTheme: string, path: string) | Set content theme, see options.preview.theme.list for contentTheme. |
npm install from the root directorynpm run start start start to start the local server and open http://localhost:9000npm run build package code to dist directoryDue to the use of on-demand loading mechanism, the default CDN is https://unpkg.com/vditor@ version number
If the code is modified or you need to use a self-built CDN, you can follow the following steps:
cdn , emojiPath , and themes in options and IPreviewOptionshighlightRender , mathRender , abcRender , chartRender , mermaidRender , SMILESRender , markmapRender , flowchartRender , mindmapRender , plantumlRender , graphvizRender , setCodeTheme , setContentTheme methods need to add cdn parametersPlease read the upgrade part in CHANGELOG carefully when upgrading the version
Vditor uses the MIT open source protocol.
In the early days of developing Sym, we directly used the WYSIWYG rich text editor. At that time, HTML-based editors were very popular, and they were very convenient to quote in projects, and they were in line with the user's usage habits at that time.
Later, the rise of Markdown gradually changed everyone's layout. In addition, several of our other projects are for programmers, so migration to md is also a general trend. We chose CodeMirror, an excellent editor that provides developers with rich programming interfaces and is also more compatible with various browsers.
Later, as our project business needs settled, using CodeMirror sometimes feels "cumbersome". For example, to achieve @automatically complete username list, insert Emoji, upload files, etc., it requires relatively in-depth secondary development, and these business needs are exactly what many project scenarios have and are necessary.
Finally, we decided to start implementing the editor ourselves in Sym. With several versions iterations, Sym's editor is becoming more and more mature. On the community chain we operate, some people asked us whether we can withdraw the editor separately and provide it to everyone for use. At the same time, our front-end main program V class was a little overwhelmed in maintaining the editors scattered in various projects, and also had a good impression of TypeScript, so we decided to use ts to implement a brand new browser-side md editor.
So, Vditor was born.