Vditor 一款浏览器端的 Markdown 编辑器,支持所见即所得(富文本)、即时渲染(类似 Typora)和分屏预览模式

本贴最后更新于 1650 天前,其中的信息可能已经沧海桑田

Vditor
易于使用的 Markdown 编辑器,为适配不同的应用场景而生

npm bundle size


English  |  Demo

💡 简介

Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。

欢迎到 Vditor 官方讨论区了解更多。同时也欢迎关注 B3log 开源社区微信公众号 B3log开源

b3logos.jpg

🗺️ 背景

随着 Markdown 排版方式的普及,越来越多的应用开始集成 Markdown 编辑器。目前主流可集成的 Markdown 编辑器现状如下:

  • 有的仅支持分屏预览,即编辑区和预览区分离
  • 有的同时支持所见即所得和分屏预览,但所见即所得模式下不能完整支持 Markdown 语法排版
  • 几乎没有类似 Typora 的即时渲染

而这三点恰好对应了三种应用场景:

  • 分屏预览:适配传统的 Markdown 使用场景,适合大屏下编辑排版
  • 所见即所得:对不熟悉 Markdown 的用户友好,熟悉 Markdown 的用户也可以无缝使用
  • 即时渲染:理论上这是最为优雅的 Markdown 编辑方式,让熟悉 Markdown 的用户能够更专注于内容创作

所以,一个能够适配应用场景的 Markdown 编辑器至关重要,它需要考虑到:

  • 传统 Markdown 用户的使用场景,提供分屏预览
  • 富文本编辑用户的使用场景,提供所见即所得
  • 高阶 Markdown 用户的使用场景,提供即时渲染

Vditor 在这些方面做了努力,希望能为现代化的通用 Markdown 编辑领域做出一些贡献。

✨ 特性

  • 支持三种编辑模式:所见即所得(wysiwyg)、即时渲染(ir)、分屏预览(sv)
  • 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、多媒体、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、plantumlUML 图
  • 导出、图片懒加载、任务列表、多平台预览、多主题切换、复制到微信公众号/知乎功能
  • 实现 CommonMark 和 GFM 规范,可对 Markdown 进行格式化和语法树查看,并支持 10+ 项配置
  • 工具栏包含 36+ 项操作,除支持扩展外还可对每一项中的快捷键、提示、提示位置、图标、点击事件、类名、子工具栏进行自定义
  • 表情/at/话题等自动补全扩展
  • 可使用拖拽、剪切板粘贴上传,显示实时上传进度,支持 CORS 跨域上传
  • 实时保存内容,防止意外丢失
  • 录音支持,用户可直接发布语音
  • 粘贴 HTML 自动转换为 Markdown,如粘贴中包含外链图片可通过指定接口上传到服务器
  • 支持主窗口大小拖拽、字符计数
  • 多主题支持,内置黑白绿三套主题
  • 多语言支持,内置中、英、韩文本地化
  • 支持主流浏览器,对移动端友好

editor.png

preview.png

🔮 编辑模式

所见即所得(WYSIWYG)

所见即所得模式对不熟悉 Markdown 的用户较为友好,熟悉 Markdown 的话也可以无缝使用。

vditor-wysiwyg

即时渲染(IR)

即时渲染模式对熟悉 Typora 的用户应该不会感到陌生,理论上这是最优雅的 Markdown 编辑方式。

vditor-ir

分屏预览(SV)

传统的分屏预览模式适合大屏下的 Markdown 编辑。

vditor-sv

🍱 语法支持

  • 所有 CommonMark 语法:分隔线、ATX 标题、Setext 标题、缩进代码块、围栏代码块、HTML 块、链接引用定义、段落、块引用、列表、反斜杠转义、HTML 实体、行级代码、强调、加粗、链接、图片、行级 HTML、硬换行、软换行和纯文本。
  • 所有 GFM 语法:表格、任务列表项、删除线、自动链接、XSS 过滤
  • 常用 Markdown 扩展语法:脚注、ToC、自定义标题 ID
  • 图表语法
    • 流程图、时序图、甘特图,通过 Mermaid 支持
    • Graphviz
    • 折线图、饼图、脑图等,通过 ECharts 支持
  • 五线谱:通过 abc.js 支持
  • 数学公式:数学公式块、行级数学公式,通过 MathJax 和 KaTeX 支持
  • YAML Front Matter
  • 中文语境优化
    • 中西文之间插入空格
    • 术语拼写修正
    • 中文后跟英文逗号句号等标点替换为中文对应标点

以上大部分特性可以通过开关配置是否启用,开发者可根据自己的应用场景选择搭配。

🗃 案例

  • Sym 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台
  • Solo & Pipe B3log 分布式社区的博客端节点,欢迎加入下一代社区网络
  • Tditor 基于 React、Vditor、Springboot,一款打造极致文字创作体验的在线 Markdown 编辑平台
  • Arya 基于 Vue、Vditor,所构建的在线 Markdown 编辑器
  • 更多案例

🛠️ 使用文档

CommonJS

  • 安装依赖
npm install vditor --save
  • 在代码中引入并初始化对象,可参考 index.js
import Vditor from 'vditor'
import "~vditor/src/assets/less/index"

const vditor = new Vditor(id, {options...})

HTML script

  • 在 HTML 中插入 CSS 和 JavaScript,可参考 demo
<!-- ⚠️生产环境请指定版本号,如 https://unpkg.com/vditor@x.x.x/dist... -->
<link rel="stylesheet" href="https://unpkg.com/vditor/dist/index.css" />
<script src="https://unpkg.com/vditor/dist/index.min.js"></script>

示例代码

主题

编辑器主题

编辑器所展现的外观。内置 classic,dark 2 套主题。

  • 编辑器初始化时可通过 options.theme 设置内置主题
  • 初始化完成后可通过 setTheme 更新编辑器主题
  • 可通过修改 index.less 中的变量对主题颜色进行定制
  • 可参考现有结构和类名在原有基础上进行修改

内容主题

Markdown 输出的 HTML 所展现的外观。内置 ant-design, light,dark,wechat 4 套主题。支持内容主题扩展接口。

  • 需在显示元素上添加 class="vditor-reset"
  • 编辑器初始化时可通过 options.preview.theme 设置内置或自己开发的主题列表
  • 内容渲染初始化时可通过 IPreviewOptions.theme 设置内置或自己开发的主题
  • 初始化完成后可通过 setThemesetContentTheme 更新内容主题

代码主题

代码块所展现的外观。内置 github 等 36 套主题。

  • 编辑器初始化时可通过 options.preview.hljs 对代码块样式、行号、是否启用进行设置
  • 内容渲染初始化时可通过 IPreviewOptions.hljs 对代码块样式、行号、是否启用进行设置
  • 初始化完成后可通过 setThemesetCodeTheme 更新代码主题

API

id

可填入元素 id 或元素自身 HTMLElement

⚠️:当填入元素自身的 HTMLElement 时需设置 options.cache.id 或将 options.cache.enable 设置为 false

options

说明 默认值
i18n 多语言,参见 ITips -
undoDelay 历史记录间隔 -
after 编辑器异步渲染完成后的回调方法 -
height 编辑器总高度 'auto'
minHeight 编辑区域最小高度 -
width 编辑器总宽度,支持 % 'auto'
placeholder 输入区域为空时的提示 ''
lang 语言种类:en_US, fr_FR, pt_BR, ja_JP, ko_KR, ru_RU, sv_SE, zh_CN, zh_TW 'zh_CN'
input(value: string) 输入后触发 -
focus(value: string) 聚焦后触发 -
blur(value: string) 失焦后触发 -
keydown(event: KeyboardEvent) 按下后触发 -
esc(value: string) esc 按下后触发 -
ctrlEnter(value: string) ⌘/ctrl+enter 按下后触发 -
select(value: string) 编辑器中选中文字后触发 -
tab tab 键操作字符串,支持 \t 及任意字符串 -
typewriterMode 是否启用打字机模式 false
cdn 配置自建 CDN 地址 https://unpkg.com/vditor@${VDITOR_VERSION}
mode 可选模式:sv, ir, wysiwyg 'ir'
debugger 是否显示日志 false
value 编辑器初始化值 ''
theme 主题:classic, dark 'classic'
icon 图标风格:ant, material 'ant'
customRenders: {language: string, render: (element: HTMLElement, vditor: IVditor) => void}[] 自定义渲染器 []

options.toolbar

  • 工具栏,可使用 name 进行简写: toolbar: ['emoji', 'br', 'bold', '|', 'line'] 。默认值参见 src/ts/util/Options.ts
  • name 可枚举为: emojiheadingsbolditalicstrike|linequotelistordered-listcheck ,outdent ,indentcodeinline-codeinsert-afterinsert-before ,undoredouploadlinktablerecordedit-modebothpreviewfullscreenoutlinecode-themecontent-themeexport, devtoolsinfohelpbr
  • name 不在枚举中时,可以添加自定义按钮,格式如下:
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')},
    }],
})
说明 默认值
name 唯一标示 -
icon svg 图标 -
tip 提示 -
tipPosition 提示位置:'n', 'ne', 'nw', 's', 'se', 'sw', 'w', 'e' -
hotkey 快捷键,格式为⇧⌘//⌥⌘ -
suffix 插入编辑器中的后缀 -
prefix 插入编辑器中的前缀 -
click(event: Event, vditor: IVditor) 自定义按钮点击时触发的事件 -
className 样式名 ''
toolbar?: Array<options.toolbar> 子菜单 -

options.toolbarConfig

说明 默认值
hide 是否隐藏工具栏 false
pin 是否固定工具栏 false

options.counter

说明 默认值
enable 是否启用计数器 false
after(length: number, counter: options.counter): void 字数统计回调 -
max 允许输入的最大值 -
type 统计类型:'markdown', 'text' 'markdown'

options.cache

说明 默认值
enable 是否使用 localStorage 进行缓存 true
id 缓存 key,第一个参数为元素且启用缓存时必填 -
after(html: string): string 缓存后的回调 -

options.comment

⚠️:仅支持 wysiwyg 模式

说明 默认值
enable 是否启用评论模式 false
add(id: string, text: string, commentsData: ICommentsData[]) 添加评论回调 -
remove(ids: string[]) 删除评论回调 -
scroll(top: number) 滚动回调 -
adjustTop(commentsData: ICommentsData[]) 文档修改时,适配评论高度 -

options.preview

说明 默认值
delay 预览 debounce 毫秒间隔 1000
maxWidth 预览区域最大宽度 800
mode 显示模式:both, editor 'both'
url md 解析请求 -
parse(element: HTMLElement) 预览回调 -
transform(html: string): string 渲染之前回调 -

options.preview.hljs

说明 默认值
defaultLang 未指定语言时默认使用该语言 ''
enable 是否启用代码高亮 true
style 可选值参见 Chroma github
lineNumber 是否启用行号 false
langs 自定义指定语言 CODE_LANGUAGES
renderMenu(code: HTMLElement, copy: HTMLElement) 渲染菜单按钮 -

options.preview.markdown

说明 默认值
autoSpace 自动空格 false
gfmAutoLink 自动链接 true
fixTermTypo 自动矫正术语 false
toc 插入目录 false
footnotes 脚注 true
codeBlockPreview wysiwyg 和 ir 模式下是否对代码块进行渲染 true
mathBlockPreview wysiwyg 和 ir 模式下是否对数学公式进行渲染 true
paragraphBeginningSpace 段落开头空两个 false
sanitize 是否启用过滤 XSS true
listStyle 为列表添加 data-style 属性 false
linkBase 链接相对路径前缀 ''
linkPrefix 链接强制前缀 ''
mark 启用 mark 标记 false

options.preview.theme

说明 默认值
current 当前主题 "light"
list 可选主题列表 { "ant-design": "Ant Design", dark: "Dark", light: "Light", wechat: "WeChat" }
path 主题样式地址 https://unpkg.com/vditor@${VDITOR_VERSION}/dist/css/content-theme

options.preview.math

说明 默认值
inlineDigit 内联数学公式起始 $ 后是否允许数字 false
macros 使用 MathJax 渲染时传入的宏定义 {}
engine 数学公式渲染引擎:KaTeX, MathJax 'KaTeX'
mathJaxOptions 数学公式渲染引擎为 MathJax 时的参数 -

options.preview.actions?: Array<IPreviewAction | IPreviewActionCustom>

默认值为 ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"]。
可从默认值中挑选进行配置,也可使用以下字段进行自定制开发。

说明 默认值
key 按钮唯一标识,不能为空 -
text 按钮文字 -
tooltip 提示 -
className 按钮类名 -
click(key: string) 按钮点击回调事件 -

options.preview.render.media

说明 默认值
enable 是否启用多媒体渲染 true

options.image

说明 默认值
isPreview 是否预览图片 true
preview(bom: Element) => void 图片预览处理 -
说明 默认值
isOpen 是否打开链接地址 true
click(bom: Element) => void 点击链接事件 -

options.hint

说明 默认值
parse 是否进行 md 解析 true
delay 提示 debounce 毫秒间隔 200
emoji 默认表情,可从 lute/emoji_map 中选取,也可自定义 { '+1': '👍', '-1': '👎', 'heart': '❤️', 'cold_sweat': '😰' }
emojiTail 常用表情提示 -
emojiPath 表情图片地址 https://unpkg.com/vditor@${VDITOR_VERSION}/dist/images/emoji
extend: IHintExtend[] 对 @/话题等关键字自动补全的扩展 []
interface IHintData {
  html: string;
  value: string;
}

interface IHintExtend {
    key: string;

    hint?(value: string): IHintData[] | Promise<IHintData[]>;
}

options.upload

  • 文件上传的数据结构如下。后端返回的数据结构不一致时,可使用 format 进行转换。
// 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 可将剪贴板中的站外图片地址传到服务器端进行保存处理,其数据结构如下:
// POST data
xhr.send(JSON.stringify({url: src})); // src 为站外图片地址
// return data
{
 msg: '',
 code: 0,
 data : {
   originalURL: '',
   url: ''
 }
}
  • successformaterror 不会同时触发,具体调用情况如下:
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);
    }
}
说明 默认值
url 上传 url,为空则不会触发上传相关事件 ''
max 上传文件最大 Byte 10 * 1024 * 1024
linkToImgUrl 剪切板中包含图片地址时,使用此 url 重新上传 ''
linkToImgCallback(responseText: string) 图片地址上传回调 -
linkToImgFormat(responseText: string): string 对图片地址上传的返回值进行格式化 -
success(editor: HTMLPreElement, msg: string) 上传成功回调 -
error(msg: string) 上传失败回调 -
token CORS 上传验证,头为 X-Upload-Token -
withCredentials 跨站点访问控制 false
headers 请求头设置 -
filename(name: string): string 文件名安全处理 name => name.replace(/\W/g, '')
accept 文件上传类型,同 input accept -
validate(files: File[]) => string | boolean 校验,成功时返回 true 否则返回错误信息 -
handler(files: File[]) => string | null | Promise | Promise 自定义上传,当发生错误时返回错误信息 -
format(files: File[], responseText: string): string 对服务端返回的数据进行转换,以满足内置的数据结构 -
file(files: File[]): File[] | Promise<File[]> 将上传的文件处理后再返回 -
setHeaders(): { [key: string]: string } 上传前使用返回值设置头 -
extraData: { [key: string]: string | Blob } 为 FormData 添加额外的参数 -
multiple 上传文件是否为多个 true
fieldName 上传字段名称 'file[]'
renderLinkDest?(vditor: IVditor, node: ILuteNode, entering: boolean): [string, number] 处理剪贴板中的图片地址 ''

options.resize

说明 默认值
enable 是否支持大小拖拽 false
position 拖拽栏位置:'top', 'bottom' 'bottom'
after(height: number) 拖拽结束的回调 -

options.classes

说明 默认值
preview 预览元素上的 className ''

options.fullscreen

说明 默认值
index 全屏层级 90

options.outline

说明 默认值
enable 初始化是否展现大纲 false
position 大纲位置:'left', 'right' 'left'

methods

说明
exportJSON(markdown: string) 根据 Markdown 获取对应 JSON
getValue() 获取 Markdown 内容
getHTML() 获取 HTML 内容
insertValue(value: string, render = true) 在焦点处插入内容,并默认进行 Markdown 渲染
focus() 聚焦到编辑器
blur() 让编辑器失焦
disabled() 禁用编辑器
enable() 解除编辑器禁用
getSelection(): string 返回选中的字符串
setValue(markdown: string, clearStack = false) 设置编辑器内容且选中清空历史栈
clearStack() 清空撤销和重做记录栈
renderPreview(value?: string) 设置预览区域内容
getCursorPosition():{top: number, left: number} 获取焦点位置
deleteValue() 删除选中内容
updateValue(value: string) 更新选中内容
isUploading() 上传是否还在进行中
clearCache() 清除缓存
disabledCache() 禁用缓存
enableCache() 启用缓存
html2md(value: string) HTML 转 md
tip(text: string, time: number) 消息提示。time 为 0 将一直显示
setPreviewMode(mode: "both" | "editor") 设置预览模式
setTheme(theme: "dark" | "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) 设置主题、内容主题及代码块风格
getCurrentMode(): string 获取编辑器当前编辑模式
destroy() 销毁编辑器
getCommentIds(): {id: string, top: number}[] 获取所有评论
hlCommentIds(ids: string[]) 高亮评论
unHlCommentIds(ids: string[]) 取消评论高亮
removeCommentIds(removeIds: string[]) 删除评论
updateToolbarConfig(config: {hide?: boolean, pin?: boolean}) 更新工具栏配置

static methods

  • 不需要进行编辑操作时,仅需引入 method.min.js 后如下直接调用
Vditor.mermaidRender(document)
import VditorPreview from 'vditor/dist/method.min'
VditorPreview.mermaidRender(document)
  • 需要对页面中的 Markdown 进行渲染时可直接调用 preview 方法,参数如下:
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.jsindex.min.js 不可同时引入
说明
previewImage(oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") 点击图片预览
mermaidRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) 流程图/时序图/甘特图
SMILESRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) 化学物质结构
markmapRender(element: HTMLElement, cdn = options.cdn) markdown 思维导图
flowchartRender(element: HTMLElement, cdn = options.cdn) flowchart 渲染
codeRender(element: HTMLElement, option?: IHljs) 为 element 中的代码块添加复制按钮
chartRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) 图表渲染
mindmapRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) 脑图渲染
plantumlRender(element: (HTMLElement | Document) = document, cdn = options.cdn) plantuml 渲染
abcRender(element: (HTMLElement | Document) = document, cdn = options.cdn) 五线谱渲染
md2html(mdText: string, options?: IPreviewOptions): Promise<string> Markdown 文本转换为 HTML,该方法需使用异步编程
preview(previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) 页面 Markdown 文章渲染
highlightRender(hljsOption?: IHljs, element?: HTMLElement | Document, cdn = options.cdn) 为 element 中的代码块进行高亮渲染
mediaRender(element: HTMLElement) 特定链接分别渲染为视频、音频、嵌入的 iframe
mathRender(element: HTMLElement, options?: {cdn?: string, math?: IMath}) 对数学公式进行渲染
speechRender(element: HTMLElement, lang?: (keyof II18nLang)) 对选中的文字进行阅读
graphvizRender(element: HTMLElement, cdn?: string) 对 graphviz 进行渲染
outlineRender(contentElement: HTMLElement, targetElement: Element) 对大纲进行渲染
lazyLoadImageRender(element: (HTMLElement | Document) = document) 对启用懒加载的图片进行渲染
setCodeTheme(codeTheme: string, cdn = options.cdn) 设置代码主题,codeTheme 参见 options.preview.hljs.style
setContentTheme(contentTheme: string, path: string) 设置内容主题,contentTheme 参见 options.preview.theme.list

🏗 开发文档

原理相关

环境

  1. 安装 node LTS 版本
  2. 下载最新代码并解压
  3. 根目录运行 npm install
  4. npm run start 启动本地服务器,打开 http://localhost:9000
  5. 修改代码
  6. npm run build 打包代码到 dist 目录

CDN 切换

由于使用了按需加载的机制,默认 CDN 为 https://unpkg.com/vditor@ 版本号

如果代码有修改或需要使用自建 CDN 的话,可按以下步骤进行操作:

  • 初始化时,需对 optionsIPreviewOptions 中的 cdnemojiPath, themes 进行配置
  • highlightRendermathRenderabcRenderchartRendermermaidRenderSMILESRendermarkmapRenderflowchartRendermindmapRenderplantumlRendergraphvizRendersetCodeThemesetContentTheme 方法中需添加 cdn 参数
  • 将 build 成功的 dist 目录或 jsDelivr 中的 dist 目录拷贝至正确的位置

升级

版本升级时请仔细阅读 CHANGELOG 中的升级部分

Ⓜ️ Markdown 使用指南

🏘️ 社区

📄 授权

Vditor 使用 MIT 开源协议。

🙏 鸣谢

  • Lute:🎼 一款结构化的 Markdown 引擎,支持 Go 和 JavaScript
  • highlight.js:JavaScript syntax highlighter
  • mermaid:Generation of diagram and flowchart from text in a similar manner as Markdown
  • incubator-echarts:A powerful, interactive charting and visualization library for browser
  • abcjs:JavaScript library for rendering standard music notation in a browser

📽️ 历史

我们在开发 Sym 的初期是直接使用 WYSIWYG 富文本编辑器的。那时候基于 HTML 的编辑器非常流行,项目中引用起来也很方便,也符合用户当时的使用习惯。

后来,Markdown 的崛起逐步改变了大家的排版方式。再加上我们其他几个项目都是面向程序员用户的,所以迁移到 md 上也是大势所趋。我们选择了 CodeMirror,这是一款优秀的编辑器,它对开发者提供了丰富的编程接口,对各种浏览器的兼容性也比较好。

再后来,随着我们项目业务需求方面的沉淀,使用 CodeMirror 有时候会感到比较“笨重”。比如要实现 @ 自动完成用户名列表、插入 Emoji、上传文件等就需要比较深入的二次开发,而这些业务需求恰恰是很多项目场景共有且必备的。

终于,我们决定开始在 Sym 中自己实现编辑器。随着几个版本的迭代,Sym 的编辑器也日趋成熟。在我们运营的社区链滴上陆续有人问我们是否能将编辑器单独抽离出来提供给大家使用。与此同时,我们的前端主程 V 同学对于维护分散在各个项目中的编辑器也感到有点力不从心,外加对 TypeScript 的好感,所以就决定使用 ts 来实现一个全新的浏览器端 md 编辑器。

于是,Vditor 就这样诞生了。

相关帖子

优质回帖
  • Vanessa 1

    前端是这样传送的

        const formData = new FormData();
        for (let i = 0, iMax = uploadFileList.length; i < iMax; i++) {
            formData.append("file[]", uploadFileList[i]);
        }
         xhr.send(formData);
    
  • haaid 1

    文字错误:接触编辑器禁用,「接触」->「解除」

  • someone9891 1

    此链接 404

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 547176052 3 评论

    怎么导出 md 语法渲染完成的 html

    可以使用 methods 中的 getHTML
    Vanessa
    @Vanessa getHTML 后返回的 html 代码 不能直接显示在网页上
    547176052
    @547176052 那你可以看一下 preview 方法
    Vanessa
  • xhaoxiong 1 评论

    @Vanessa V 姐这个有统计字数的接口嘛 😋

    暂时没有,你需要的场景是?
    Vanessa
  • xiaoyaoFreedom 9 评论

    你好,为什么我没改代码的情况下,编辑器突然不提示语法了呢?预览模式也无法默认生效,只能点击编辑器按钮才生效,而且界面和以前不一样了 😭
    image.png
    image.png
    image.png

    点击编辑器的工具栏图标,比如粗体,斜体,删除线等,然后相关的语法不会出现在我的光标位置,而是出现在第一行的第一格,但有时候它又会出现在光标的位置。我想点击后语法出现在我的光标位置请问怎么解决呢?(如果快捷键操作的话语法一直都能出现在光标位置)。
    image.png

    1 操作
    xiaoyaoFreedom 在 2019-12-30 14:57:33 更新了该回帖
    v2.0 默认为 WYSIWYG 模式,可根据需要修改 option.mode 参数
    Vanessa
    @Vanessa 你好,谢谢,这个问题解决了。但有另外一个问题,我在上面说明和附图了,请问可以解决吗?
    xiaoyaoFreedom
    @xiaoyaoFreedom 我这里无法重现,麻烦提供一下浏览器和系统信息
    Vanessa
    @Vanessa win10 企业版 ltsc(64 位),chrome 版本 79.0.3945.88(正式版本)(64 位)。我用 Firefox 试了以上操作,没有出现任何问题,但 chrome 就出现了。
    xiaoyaoFreedom
    @xiaoyaoFreedom 点击粗体按钮前,光标有没有点击过编辑器区域以外的元素?或者有什么特定的重现方法么?
    Vanessa
    @Vanessa 不好意思,有事晚回复了。我刚看了一下,点击工具栏部分或者工具栏的图标后会失焦。我尝试 blur 事件,失焦后立即聚焦,虽然可以解决我上面说的部分情况,但同时会出现其它问题(比如表情无法出现,编辑器预览不会自动下滑等),所以我这个方法无法解决。
    xiaoyaoFreedom
    @xiaoyaoFreedom 你用的是哪一个版本?你看一下 https://hacpai.com/guide/markdown 这里可以重现么?
    Vanessa
    @Vanessa 谢谢,我创建一个干净的项目发现没问题。应该是我目前的项目的问题,目前的项目我把编辑器版本回退到 1.9.0 版本就可以正常使用了。
    xiaoyaoFreedom
    @xiaoyaoFreedom 建议使用最新版本,然后通过 options.mode 来设置编辑模式
    Vanessa
  • XShellv

    你好,react 中父组件给子组件传递默认的 markdown 值时,vditor 没有提供接受该默认值的属性么?文档中有个 setValue 方法,但是在子组件接受父组件传过来的值时调用该方法产生堆栈溢出的错误。请问我该怎么办?

  • XShellv 1 评论

    我的代码是这样的:

    class MdEditor extends React.Component<Props, State> {
        vditor: any
        componentWillReceiveProps(nextProps: Props) {
            if (nextProps.value) {
                this.vditor.setValue(nextProps.value)
            }
        }
        componentDidMount() {
            this.vditor = new Vditor('vditor', this.vditorConfig.vditorOptions)
            this.vditor.setValue(this.props.value)
        }
        shouldComponentUpdate(nextProps: Props, nextState: State) {
            return false
        }
        render() {
            return (
                <div id="vditor"></div>
            )
        }
        triggerChange = (changedValue: any) => {
            const { onChange } = this.props;
            if (onChange) {
                onChange(changedValue);
            }
        };
    }
    
    
    Vanessa
  • XShellv 1 评论

    又来提建议了,我发现在 react 中调用 setValue 方法后还会触发 input 事件,这个逻辑好像不对,input 事件难道不应该是用户输入时才会触发么?两者区别还是很大的。。。

    setValue 会涉及到预览渲染,用户在 input 的时候可能会处理渲染结果,所以就没有剥离出来
    Vanessa
  • @Vanessa V 姐,我在 element 表单中载入了 vditor,vditor 是作为子组件在父组件中导入的,上传文件按钮样式是不是和 element ui 的样式冲突了,要怎么弄啊 😂 。
    QQ 截图 20200106095935.png

    已解决。
    zyk 1
    @zhaoyangkun 再 reset 回去?
    Vanessa
    @Vanessa element ui 表单元素默认有行高值,我在 vditor 组件中把行高值覆盖掉就可以了。
    zyk
  • buexplain 3 评论

    发现一个错误,请楼主帮忙看看。
    版本:Vditor v2.0.15
    报错:image.pngimage.png

    新的问题:编辑器默认选中了“所见即所得”按钮
    image.png

    1 操作
    buexplain 在 2020-01-14 18:46:30 更新了该回帖
    是不是调用方法的时候页面还没有元素? 或者 setValue 方法放入 options.after 中试一下
    Vanessa
    options.after 解决了我的问题,但是又出来了一个新的问题:编辑器初始化后默认选中了“所见即所得”按钮。
    buexplain
    @buexplain 可使用 options.mode 参数进行设置
    Vanessa 1
  • XShellv 2 评论

    我想继续追问下,在 react 中如何将 state 中每次更新变化的 value 值传递给传递给 vditor?vditorConfig 只能在 componentDidMount 中初始化一次,然而后续如果想继续给编辑器传值还是不行。setState 方法再 react 中出现死循环错误,不知道其他人有没有这个错误。

    对 new 出来的对象使用 setValue 方法,或者检查一下 setState
    Vanessa
    @Vanessa 解决了,通过 setValue 完成,是我的 setState 出现循环调度了
    XShellv
  • q2484877 3 评论

    在外面能否拿到上传文件的事件啊? 我想先拿到文件对象先进行压缩后再上传到七牛云中! 看了半天代码,貌似上传的事件不能在初始化的时候取到!

    表示看不懂!
    q2484877
    @q2484877 这个是 issue,可以关注
    Vanessa
  • @Vanessa V 姐,我在单页面 vue 中创建了两个 elementui 的弹窗组件,在两个弹窗中都载入了 vditor 组件,当先点击添加文章弹窗,然后再点击修改文章弹窗,修改文章弹窗中 vditor 组件没有渲染,该怎么破啊 😂 ?vditor 组件代码是这样的。

    <template>
        <div class="contentEditor-box">
            <div :id="id"></div>
        </div>
    </template>
    
    <script>
    import Vditor from 'vditor';
    import { LazyLoadImage } from '../../assets/js/utils.';
    
    export default {
        name: 'Vditor',
        data() {
            return {
                id: 'contentEditor',
                contentEditor: null,
                headers: { Authorization: '' },
                upload_url: `http://127.0.0.1:8000/api/img_cos/vditor/upload/`
            };
        },
        created() {
            this.getToken();
        },
        mounted() {
            this.contentEditor = this.initEditor();
        },
        methods: {
            //获取token
            getToken() {
                const token = localStorage.getItem('token');
                if (token) {
                    this.headers.Authorization = `Bearer ${token}`;
                } else {
                    this.headers.Authorization = '';
                }
            },
            //初始化vditor
            initEditor() {
                return new Vditor(this.id, {
                    typewriterMode: true,
                    tab: '\t',
                    cache: true,
                    preview: {
                        delay: 500,
                        // mode: 'both',
                        parse: element => {
                            if (element.style.display === 'none') {
                                return;
                            }
                            LazyLoadImage();
                            Vditor.highlightRender(
                                {
                                    style: 'github',
                                    enable: true
                                },
                                document
                            );
                        }
                    },
                    upload: {
                        accept: '.jpg,.png,.gif,.jpeg',
                        max: 2 * 1024 * 1024,
                        url: this.upload_url,
                        headers: this.headers,
                        filename: name =>
                            name
                                .replace(/[^(a-zA-Z0-9\u4e00-\u9fa5\.)]/g, '')
                                .replace(/[\?\\/:|<>\*\[\]\(\)\$%\{\}@~]/g, '')
                                .replace('/\\s/g', ''),
                        success(editor, data) {
                            data = JSON.parse(data); //将json字符串转换成json
                            let img_text = '';
                            for (let i = 0; i < data.data.url_list.length; i++) {
                                img_text += `\n![](${data.data.url_list[i]})\n`;
                            }
                            editor.innerHTML = img_text; //将图片链接写入编辑区
                        },
                        error(data) {
                            alert('上传失败');
                        }
                    },
                    hint: {
                        emoji: {
                            pray: '🙏',
                            broken_heart: '💔',
                            ok_hand: '👌',
                            smile: '😄',
                            laughing: '😆',
                            smirk: '😏',
                            heart_eyes: '😍',
                            grin: '😁',
                            stuck_out_tongue: '😛',
                            expressionless: '😑',
                            unamused: '😒',
                            sob: '😭',
                            joy: '😂',
                            tired_face: '😫',
                            blush: '😊',
                            cry: '😢',
                            fearful: '😨'
                        }
                    },
                    height: 350,
                    counter: 100000,
                    placeholder: ''
                });
            },
            //获取编辑器文本
            getContent() {
                return this.contentEditor.getValue();
            },
            //设置编辑器文本
            setContent(val) {
                this.contentEditor.setValue(val);
            }
        }
    };
    </script>
    
    可能是因为他的弹窗组件是单实例的,不行的话你重新 new 就可以了
    Vanessa 1
    @Vanessa 好的,我试试
    zyk
  • @Vanessa V 姐,我想在 HTML 页面中使用 vditor 默认的 Markdown 样式该怎么弄啊 😂 ?

    引入 ~vditor/dist/index.css 后在元素上添加 class="vditor"
    Vanessa 1
    好的
    zyk
  • alexmh 1 评论

    直接截图然后粘贴 是可以成功 请求 upload 对象中 url 进行上传的 但是配置了 linkToImgUrl 然后复制一个网站的图片 怎么也触发不了上传的事件 😂
    @Vanessa

    这个目前仅支持 markdown 模式,wysiwyg 还不支持,可以关注 Issue #134 · Vanessa219/vditor issue
    Vanessa
  • alexmh

    谢谢 @Vanessa

  • alexmh 2 评论

    一直不清楚 右侧的那个锚点列表是怎么生成的呀 @Vanessa

    那个目前是后台解析生成的,可关注 Issue #121 · Vanessa219/vditor
    Vanessa
    哦 明白了 谢谢
    alexmh
  • alexmh 1 评论

    请教个问题 at 配置问题 选中某个值后 一确认就会报错 是不是我配置有问题 @Vanessa 期待回复! 谢谢
    以下是我的代码和执行的效果图

    options:{
    					height:500,   
    					cache:false,
    					value:'',
    					typewriterMode:true,
    					withCredentials:true,
    					anchor:true,
    					// mode:'markdown-only',
    					hint:{
    						delay:200, 
    						at:function(){ 
    							return [
    								{value: '1', html: '用户一'},
    								{value: '2', html: '用户二'}
    								]
    						}
    					}
    				},
    

    这是执行的效果

    value 需要以 @ 开头,如 @1
    Vanessa 1 1 赞同
  • @Vanessa V 姐,我在 Ubuntu 下的 Chrome 浏览器加载 vditor 部分表情是黑白的,是不是系统缺失了字体?

    嗯嗯
    Vanessa
  • paulirish00 1 评论

    不怎么会用

    Vanessa
  • kuuyee 2 评论

    我直接用 preview 渲染显示,发现显示的有点问题,前端用的 vue iview,直接用的 demo 里提供的例子

    
            <Card>
              <div id="previewWrap">
                <div id="preview" class="preview vditor-reset--dark"></div>
              </div>
              <textarea id="zh_CNText" style="display:none;">
    
    引用文本:Markdown is a text **formatting** syntax inspired
    
    ### 渲染
    
    - **加粗** - `**加粗**`
    - *倾斜* - `*倾斜*`
    - ~~删除线~~ - `~~删除线~~`
    - `Code 标记` - `` `Code 标记` ``
    - [超级链接](https://hacpai.com) - `[超级链接](https://hacpai.com)`
    - [username@gmail.com](mailto:username@gmail.com) - `[username@gmail.com](mailto:username@gmail.com)`
            </textarea>
            </Card>
    
    

    显示结果如下

    image.png

    美女给看看这是我的用法不对吗?调用方式如下:

    import VditorPreview from "vditor/dist/method.min";

      mounted() {
        VditorPreview.preview(
            document.getElementById("preview"),
            document.getElementById("zh_CNText").textContent,
            {
              speech: {
                enable: true
              },
              anchor: true
            }
          )
      }
    
    1 回复
    你打印 document.getElementById("zh_CNText").textContent 看一下是否有换行?
    Vanessa
    @Vanessa 打印了,确实没有换行。这种内容初始化的方式在 vue 里不行?!
    kuuyee
  • 可以的。

    VditorPreview.preview(
            document.getElementById("preview"),
            test,
            {
              speech: {
                enable: true
              },
              anchor: true
            }
          )
    
  • buexplain 1 评论

    image.png

    最新版的 demo/static.html 设置 mode 编辑器异常

    最新版已经没有这个属性了,可以看一下 CHANGELOG.md
    Vanessa
  • emmm 1 评论

    大佬,你这导航栏怎么实现的,有源码可以参考下吗,没找到渲染 heading 回调函数

    这个导航是社区实现的
    Vanessa
  • buexplain 1 评论

    版本:latest
    初始化编辑器的时候,模式设置是:mode: 'sv',会报错:

    index.min.js:27 Uncaught (in promise) TypeError: Cannot read property 'preventDefault' of null
    

    当我点击编辑器的工具栏,主动设置为所见即所得模式之后,再刷新页面,这个时候编辑器不会报错,初始化正常。
    我观察了一下 local storage ,里面有个 vditorj-vditor 项,只有这个项存在,分屏预览模式才正常。

    附上初始化编辑器的代码:

    //渲染编辑器
    //@link https://hacpai.com/article/1549638745630 文档地址
    //@link https://github.com/Vanessa219/vditor/blob/master/CHANGELOG.md?utm_source=hacpai.com 升级日志
    let vEditorObj = null;
    //写下这行代码的原因 @link https://hacpai.com/article/1549638745630/comment/1585808918654?r=buexplain
    window.localStorage.setItem('vditorj-vditor', '');
    vEditorObj = new Vditor('j-vditor', {
        debugger: true,
        typewriterMode: true,
        placeholder: '请输入文章内容',
        counter: 65535,
        height: ($(window).height() - 93),
        width:"100%",
        cache:false,
        tab: '\t',
        theme: 'classic',
        mode:'sv',
        upload: {
            accept: acceptMimeTypes,
            handler (files) {
                var formDataArr = [];
                var token = _token();
                for(var i in files) {
                    var formData = new FormData();
                    formData.append('_token', token);
                    formData.append('file', files[i]);
                    formDataArr.push(formData);
                }
                Uploads.getInstance(formDataArr).then(function (result) {
                    vEditorObj.insertValue(result.getMarkdownText());
                }).catch(function(rej) {
                    submit.alertWarning(rej.toString());
                });
            },
        },
        preview: {
            markdown: {
                toc: true,
            },
            hljs:{
                enable:true,
                style:'monokai',
                lineNumber:true
            },
            mode: 'both',
            parse: (element) => {
                lazyLoadImage()
            },
        },
        after: function () {
            //压入文章内容
            console.log(vEditorObj);
            vEditorObj.setValue(result.content.getBody());
        }
    });
    
    2 回复
    2 操作
    buexplain 在 2020-04-02 14:41:30 更新了该回帖
    buexplain 在 2020-04-02 14:40:06 更新了该回帖
    请更新到 3.0.8
    Vanessa 1
  • 我这里使用 static.html 是正常的,你再对比一下吧

    <!DOCTYPE html>
    <html lang="zh-cmn-Hans">
    <head>
        <meta charset="utf-8"/>
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
        <meta name="theme-color" content="#f1f7fe">
        <title>Vditor - A markdown editor written in TypeScript.</title>
        <meta name="description"
              content="B3log 开源社区 markdown 编辑器,使用 TypeScript 编写。支持在线预览、表情插入、at 用户提示、HTML 转换等必要功能。"/>
        <link rel="dns-prefetch" href="//cdn.jsdelivr.net/"/>
        <link rel="preconnect" href="https://cdn.jsdelivr.net">
        <link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/vditor/dist/images/logo.png"/>
        <link rel="apple-touch-icon" href="https://cdn.jsdelivr.net/npm/vditor/dist/images/logo.png">
        <link rel="shortcut icon" type="image/x-icon" href="https://cdn.jsdelivr.net/npm/vditor/dist/images/logo.png">
        <meta name="copyright" content="B3log"/>
        <meta http-equiv="Window-target" content="_top"/>
        <meta property="og:locale" content="zh-cmn-Hans"/>
        <meta property="og:title" content="Vditor - A markdown editor written in TypeScript."/>
        <meta property="og:site_name" content="Blog-vditor"/>
        <meta property="og:url" content="https://hacpai.com/tag/vditor"/>
        <meta name="twitter:card" content="summary"/>
        <meta name="twitter:domain" content="b3log.org"/>
        <meta name="twitter:title" property="og:title" itemprop="b3lig vditor"
              content="Vditor - A markdown editor written in TypeScript."/>
        <meta name="twitter:site" content="@B3logOS"/>
        <meta name="twitter:url" content="https://hacpai.com/tag/vditor"/>
        <meta property="og:image" content="https://cdn.jsdelivr.net/npm/vditor/dist/images/logo.png"/>
        <meta name="twitter:image" content="https://cdn.jsdelivr.net/npm/vditor/dist/images/logo.png"/>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vditor@latest/dist/index.css"/>
        <script src="https://cdn.jsdelivr.net/npm/vditor@3.0.7/dist/index.min.js"></script>
    </head>
    <body>
    <h2><a href="https://hacpai.com/article/1549638745630?r=Vanessa" target="_blank">Doc</a></h2>
    <h2>
        Vditor for preview
        <a href="static-preview.html?lang=zh_CN">中文</a>
        <a href="static-preview.html?lang=ko_KR">한글</a>
    </h2>
    <h2>
        Vditor for you
        <button onclick="vditor.setTheme('dark')">Dark</button>
        <button onclick="vditor.setTheme('light')">Light</button>
    </h2>
    <div id="j-vditor">
    <h1>Vditor</h1>
    <ul>
    <li>foo</li>
    <li>bar</li>
    </ul>
    </div>
    <script>
      //渲染编辑器
      //@link https://hacpai.com/article/1549638745630 文档地址
      //@link https://github.com/Vanessa219/vditor/blob/master/CHANGELOG.md?utm_source=hacpai.com 升级日志
      let vEditorObj = null
      //写下这行代码的原因 @link https://hacpai.com/article/1549638745630/comment/1585808918654?r=buexplain
      window.localStorage.setItem('vditorj-vditor', '')
      vEditorObj = new Vditor('j-vditor', {
        debugger: true,
        typewriterMode: true,
        placeholder: '请输入文章内容',
        counter: 65535,
        height: 393,
        width: '100%',
        cache: false,
        tab: '\t',
        theme: 'classic',
        mode: 'sv',
        upload: {
          handler (files) {
            var formDataArr = []
            var token = _token()
            for (var i in files) {
              var formData = new FormData()
              formData.append('_token', token)
              formData.append('file', files[i])
              formDataArr.push(formData)
            }
            Uploads.getInstance(formDataArr).then(function (result) {
              vEditorObj.insertValue(result.getMarkdownText())
            }).catch(function (rej) {
              submit.alertWarning(rej.toString())
            })
          },
        },
        preview: {
          markdown: {
            toc: true,
          },
          hljs: {
            enable: true,
            style: 'monokai',
            lineNumber: true,
          },
          mode: 'both',
          parse: (element) => {
            // lazyLoadImage()
          },
        },
        after: function () {
          //压入文章内容
          console.log(vEditorObj)
          vEditorObj.setValue('1')
        },
      })
    
    </script>
    </body>
    </html>
    
    
  • 稍等我再看看,社区的编辑器也有人报这个问题。

  • emmm 1 评论

    遇到个问题

     upload: {
            url: "http://127.0.0.1:8000/other/upload",
            linkToImgUrl: "http://127.0.0.1:8000/other/upload",
            // headers: {
            //   token: localStorage.getItem("token"),
            // },
    }
    

    这样不设置请求头是可以请求到后端的,后端也是设置了跨域,但是这个接口我做了登录信息验证,需要设置请求头 token,我根据注释代码加了请求头,就会出现

    Access to XMLHttpRequest at 'http://127.0.0.1:8000/other/upload' from origin 'http://localhost:3000' has been blocked by CORS policy: Request header field x-mode is not allowed by Access-Control-Allow-Headers in preflight response.
    index.min.js:27 POST http://127.0.0.1:8000/other/upload net::ERR_FAILED
    

    这样还是出现跨域,我其他接口都没问题,自定义上传也没问题,但是我想使用粘贴板上的上传功能,自定义上传就没这个功能,求解原因,怎么才能使用内置上传

    1 回复
    你设置一下 options.upload.withCredentials 试试
    Vanessa
  • 88250

    后端 CORS 的 Access-Control-Allow-Headers 设置的值是 * 么?如果是的话改成具体允许的标头名称(逗号分隔)后再试试看。

    1 回复
  • emmm 2 评论

    这样设置的

    return func(c *gin.Context) {
    		method := c.Request.Method               //请求方法
    		origin := c.Request.Header.Get("Origin") //请求头部
    		var headerKeys []string                  // 声明请求头keys
    		for k, _ := range c.Request.Header {
    			headerKeys = append(headerKeys, k)
    		}
    		headerStr := strings.Join(headerKeys, ", ")
    		if headerStr != "" {
    			headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
    		} else {
    			headerStr = "access-control-allow-origin, access-control-allow-headers"
    		}
    		if origin != "" {
    			c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
    			c.Header("Access-Control-Allow-Origin", "*")                                       // 这是允许访问所有域
    			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
    			//  header的类型
    			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
    			// 允许跨域设置 ,可以返回其他子段
    			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
    			c.Header("Access-Control-Max-Age", "172800")                                                                                                                                                           // 缓存请求信息 单位为秒
    			c.Header("Access-Control-Allow-Credentials", "true")                                                                                                                                                   //  跨域请求是否需要带cookie信息 默认设置为true
    			c.Set("content-type", "application/json")                                                                                                                                                              // 设置返回格式是json
    		}
    		//放行所有OPTIONS方法
    		if method == "OPTIONS" {
    			c.JSON(http.StatusOK, "Options Request!")
    		}
    		// 处理请求
    		c.Next() //  处理请求
    	}
    
    更新到 3.0.10 再试一下
    Vanessa
    @Vanessa 3.0.10 可以请求到了,感谢
    emmm
  • emmm 1 评论

    image.png

    大佬,我这上传好了,但是这个编辑器为啥不是图片而是超链接插入的

    这个是根据返回值的 key 判断类型的,在后面带个 .png 就好了
    Vanessa
  • 547176052 1 评论

    md 编辑器跟 layui 前端框架 css 冲突 这个问题不只在 layui 框架上有 在 dz 论坛集成 md 编辑器上也有这样的问题 每次都要重新修改 css 看下官方能不能解决这样的问题

    li 标签前面那个实体圆圈 没有了
    不能朗读选中的文字

    代码仓库地址

    浏览地址

    浏览文档布局

    这个你可以重置他的 CSS 就可以了,或者看看他们官方文档有没有类似的处理方法,这个应该是他们的设计缺陷。
    Vanessa
  • emmm 2 评论

    在 react 服务端渲染框架 next 中使用好像不兼容,window is not defined,想使用 preview 解析 markdown 并且渲染,不知道楼主能否解决这个问题

    1 回复
    据说可以使用懒加载就可以了,你搜搜看。这应该是个常见的问题
    Vanessa
    @Vanessa 懒加载就没有 ssr 了,那就最重要的内容没有 ssr
    emmm
  • 88250

    Vditor 的 Markdown 引擎部分是用 Lute 实现的,你可以试下直接用 Lute 进行服务端渲染,这样能得到和编辑时一致的渲染效果。

  • emmm 3 评论

    还有个问题,这个编辑器咋自动聚焦,我在其他输入框输入还没打完自己跳到编辑器了,有啥参数可以去掉这个自动聚焦吗

    不会自动聚焦,除非使用了 focus, setValue 等方法
    Vanessa
    @Vanessa 木有使用,我用的搜狗输入法 this.vditor = new Vditor("vditor", { debugger: false, typewriterMode: true, placeholder: "请输入文章内容", counter: 10000, height: 800, defaultValue: "111", cache: false, mode: "sv", preview: { markdown: { toc: true, }, hljs: { enable: true, lineNumber: true, style: "monokai", }, }, after: () => { this.vditor.setValue(this.state.content); }, blur: (value) => { this.props.callback(value); }, upload: { url: "http://118.25.110.93:8000/other/", linkToImgUrl: "http://118.25.110.93//other/", headers: { token: localStorage.getItem("token"), }, }, });
    emmm
    @emmm 按照这个代码应该没有问题,你在发帖页面试一下,里面有输入框,看一下会不会跳到编辑器
    Vanessa
  • inktear 1 评论

    在渲染时想默认展示大纲部分要怎么设置啊

    可参见 demo 中的 static-preview.html
    Vanessa
  • inktear
    1. 在编辑的时候,大纲没有实时刷新,有没有大纲刷新的方法?
    2. 编辑模式下,能在初始化的时候就直接显示大纲吗,不需要手动点击?
    1 回复
    1. 是在什么情况大纲下没有刷新,我这里重现不了
    2. 请关注 Issue #343 · Vanessa219/vditor
    1 回复
  • inktear 1 评论

    标题只输入一个字的时候无法实时刷新,输入第二个字就会刷新了,比如连接 10 个标题都只输入一个字就不会刷新,但是只要有一个标题超多一个字就会刷新

    Vanessa
  • LGSKOKO

    自己太笨了,看了文档还是不怎么会用,想问下怎样可以获取内容,尝试了 getValue()方法,报错。

    1 回复
  • 你可以先看一下 static.html,把页面下载下来,在里面使用 vditor.getValue()

  • Blackman99

    优秀

  • 547176052 1 评论

    image.png

    ant-design-pro 项目引入报错

    需要为你的脚手架配置 scss-loader,或者直接引入 css 文件试试
    Vanessa
  • darren 1

    桌面版的超链接有问题,少个 i

  • danl 1 评论

    @Vanessa image.png

    build 后这个 dist 下面的 ts 文件怎么打开 。。。。

    用 IDE
    Vanessa
  • danl

    image.png

  • inktear 1 评论

    html2md 怎么使用啊,我这边有一个之前用的 html 编辑器导致的数据,想换成 vditor makedown 编辑器,contentEditor.html2md(html) 返回值是空的

    可参见 https://vditor.b3log.org/demo/method-get.html 中的 HTML -> Markdown
    Vanessa
  • kuuyee 3 评论

    @Vanessa V妹, 我估计我年龄比你大,呵呵! 请教一个问题,请问 PDF 导出用的什么实现?是基于 md 转 PDF 吗?

    可以到源码 src/ts/export/index.ts 25:0 看看
    Vanessa
    @Vanessa 最终是基于 window.print() 吗? 这我倒是没想到!
    kuuyee
    @kuuyee 是的,要不问题太多
    Vanessa
  • kuuyee 2 评论

    @Vanessa http://vditor.b3log.org/demo/index.html 这里的例子源码能附带加上吗?

  • emmm 2 评论

    image.png

    我用 lute 和 vaditor 解析 markdown 我都试了,标题里面的()以及=> 都被解析成了--

    这是为啥传到后台数据

    image.png

    后台数据没看明白。id 中特殊字符都会转换为-
    Vanessa 1 1 赞同
    @Vanessa 那我明白了
    emmm
  • emmm 5 评论

    还有一个问题,我生成的标题上面咋上面都有个 a 标签

    image.png

    而且这个 a 标签删不掉,我现在锚点跳转都跳转不了了,不知道为啥

    你是不是配置了 anchor 这个参数,不需要的话设置为 0 即可
    Vanessa
    点了跳转是你路由配置的问题,不要使用 hash 路由
    Vanessa
    @Vanessa 我这是服务端渲染
    emmm
    @emmm 那就需要排查一下页面为什么会刷新了
    Vanessa
请输入回帖内容 ...