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

Vditor
下一代的 Markdown 编辑器,为未来而构建

npm bundle size



💡 简介

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

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

b3logos.png

📽️ 背景

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

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

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

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

于是,Vditor 就这样诞生了。

✨ 特性

editor.png

preview.png

🔮 编辑模式

所见即所得(WYSIWYG)

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

vditor-wysiwyg

即时渲染(IR)

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

vditor-ir

分屏预览(SV)

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

vditor-sv

🗃 案例

🛠️ 使用文档

CommonJS

npm install vditor --save
import Vditor from 'vditor'
import "~vditor/src/assets/scss/index"

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

HTML script

<!-- ⚠️生产环境请指定版本号,如 https://cdn.jsdelivr.net/npm/vditor@x.x.x/dist... -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vditor/dist/index.css" />
<script src="https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js" defer></script>

示例代码

主题

API

id

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

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

options

说明 默认值
after 编辑器异步渲染完成后的回调方法 -
height 编辑器总高度 'auto'
minHeight 编辑区域最小高度 -
width 编辑器总宽度,支持 % 'auto'
placeholder 输入区域为空时的提示 ''
lang 多语言:en_US, ko_KR, zh_CN 'zh_CN'
input 输入后触发 (value: string, previewElement?: HTMLElement): void -
focus 聚焦后触发 (value: string): void -
blur 失焦后触发 (value: string): void -
esc esc 按下后触发 (value: string): void -
ctrlEnter ⌘/ctrl+enter 按下后触发 (value: string): void -
select 编辑器中选中文字后触发 (value: string): void -
tab tab 键操作字符串,支持 \t 及任意字符串 -
typewriterMode 是否启用打字机模式 false
cdn 配置自建 CDN 地址 https://cdn.jsdelivr.net/npm/vditor@${VDITOR_VERSION}
mode 可选模式:sv, ir, wysiwyg 'wysiwyg'
debugger 是否显示日志 false
value 编辑器初始化值 ''
theme 主题:classic, dark 'classic'
outline 是否展现大纲 false

options.toolbar

{  
 hotkey: '⌘-⇧-f',  
 name: 'format',  
 tipPosition: 'ne',  
 tip: 'format',  
 className: '',
 icon: '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"><path d="M342 426v-84h426v84h-426zM342 256v-86h426v86h-426zM0 0h768v86h-768v-86zM342 598v-86h426v86h-426zM0 214l170 170-170 170v-340zM0 768v-86h768v86h-768z"></path></svg>',  
 click: () => {  
   alert('custom toolbar')  
 },  
}
说明 默认值
name 唯一标示 -
icon svg 图标 -
tip 提示 -
tipPosition 提示位置:ne, nw -
hotkey 快捷键,格式为⌘/ctrl-key⌘/ctrl-⇧/shift-key -
suffix 插入编辑器中的后缀 -
prefix 插入编辑器中的前缀 -
click 自定义按钮点击时触发的事件 (): void -
className 样式名 ''
toolbar?: Array<options.toolbar> 子菜单 -

options.toolbarConfig

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

options.counter

说明 默认值
enable 是否启用计数器 false
max 允许输入的最大值 -
type 统计类型:md,text 'md'

options.cache

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

options.preview

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

options.preview.hljs

说明 默认值
enable 是否启用代码高亮 true
style 可选值参见 Chroma github
lineNumber 是否启用行号 false

options.preview.markdown

说明 默认值
autoSpace 自动空格 false
fixTermTypo 自动矫正术语 false
chinesePunct 自动矫正标点 false
toc 插入目录 false
footnotes 脚注 true
codeBlockPreview wysiwyg 和 ir 模式下是否对代码块进行渲染 true
theme 内容主题:light, dark, wechat 'light'
setext 是否解析 setext 标题 false
paragraphBeginningSpace 段落开头空两个 false
sanitize 是否启用过滤 XSS true
listMarker 为列表添加 data-marker 属性 false
linkBase 链接前缀 ''

options.preview.math

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

options.hint

说明 默认值
delay 提示 debounce 毫秒间隔 200
emoji 默认表情,可从 lute/emoji_map 中选取,也可自定义 { '+1': '👍', '-1': '👎', 'heart': '❤️', 'cold_sweat': '😰' }
emojiTail 常用表情提示 -
emojiPath 表情图片地址 https://cdn.jsdelivr.net/npm/vditor@${VDITOR_VERSION}/dist/images/emoji
at @用户回调 (value: string): Array<any>,需同步返回数组 [{value: '', html: ''}] -

options.upload

// POST data  
xhr.send(formData);  // formData = FormData.append("file[]", File)  
// return data  
{  
 "msg": "",  
 "code": 0,  
 "data": {  
 "errFiles": ['filename', 'filename2'],  
 "succMap": {  
   "filename3": "filepath3",  
   "filename3": "filepath3"  
   }  
 }  
}
// POST data  
xhr.send(JSON.stringify({url: src})); // src 为站外图片地址  
// return data  
{  
 msg: '',  
 code: 0,  
 data : {  
   originalURL: '',  
   url: ''  
 }  
}
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 重新上传 ''
success 上传成功回调 (editor: HTMLPreElement, msg: string): void -
error 上传失败回调 (msg: string): void -
token CORS 上传验证,头为 X-Upload-Token -
withCredentials 跨站点访问控制 false
headers 请求头设置 -
filename 文件名安全处理 (name: string): string | name => name.replace(/\W/g, '')
accept 文件上传类型,同 input accept -
validate 校验,成功时返回 true 否则返回错误信息 (files: File[]) => string | boolean -
handler 自定义上传,当发生错误时返回错误信息 (files: File[]) => string | null -
format 对服务端返回的数据进行转换,以满足内置的数据结构 (files: File[], responseText: string): string -
file 将上传的文件处理后再返回 (files: File[]): File[] -
setHeaders 上传前使用返回值设置头 (): { [key: string]: string } -
extraData 为 FormData 添加额外的参数 { [key: string]: string | Blob } -

options.resize

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

options.classes

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

options.keymap

说明 默认值
deleteLine 删除光标所在行或选中的行 '⌘-Backspace'
duplicate 复制当前行或选中的内容 '⌘-D'

methods

说明
getValue() 获取 Markdown 内容
getHTML() 获取 HTML 内容
insertValue(value: string, render = true) 在焦点处插入内容,并默认进行 Markdown 渲染
focus() 聚焦到编辑器
blur() 让编辑器失焦
disabled() 禁用编辑器
enable() 解除编辑器禁用
setSelection(start: number, end: number) 选中从 start 开始到 end 结束的字符串,不支持 wysiwyg & ir 模式
getSelection(): string 返回选中的字符串
setValue(markdown: string) 设置编辑器内容
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) 设置主题、内容主题及代码块风格
getCurrentMode(): string 获取编辑器当前编辑模式

static methods

Vditor.mermaidRender(document)
import VditorPreview from 'vditor/dist/method.min'  
VditorPreview.mermaidRender(document)
previewElement: HTMLDivElement,   // 使用该元素进行渲染
markdown: string,  // 需要渲染的 markdown 原文
options?: IPreviewOptions {  
 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?(): void; // 渲染完成后的回调
 lazyLoadImage?: string; // 设置为 Loading 图片地址后将启用图片的懒加载
 markdown?: options.preview.markdown;
 renderers?: ILuteRender; // 自定义渲染 https://hacpai.com/article/1588412297062
}
说明
mermaidRender(element: HTMLElement, className = ".language-mermaid", cdn = options.cdn) 转换 element 中 class 为 className 的元素为流程图/时序图/甘特图
codeRender(element: HTMLElement, lang: (keyof II18nLang) = "zh_CN") 为 element 中的代码块添加复制按钮
chartRender(element: (HTMLElement | Document) = document, cdn = options.cdn) 图表渲染
mindmapRender(element: (HTMLElement | Document) = document, cdn = options.cdn) 脑图渲染
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, cdn = options.cdn) 设置内容主题,contentTheme 参见 options.preview.markdown.theme

🏗 开发文档

原理相关

环境

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

CDN 切换

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

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

升级

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

Ⓜ️ Markdown 使用指南

🏘️ 社区

📄 授权

Vditor 使用 MIT 开源协议。

🙏 鸣谢

优质回帖
  • 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

195 回帖
请输入回帖内容...
  • 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);
            }
        };
    }
    
    
  • XShellv 1 评论

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

    setValue 会涉及到预览渲染,用户在 input 的时候可能会处理渲染结果,所以就没有剥离出来
    Vanessa
  • zhaoyangkun 3 评论

    @Vanessa V 姐,我在 element 表单中载入了 vditor,vditor 是作为子组件在父组件中导入的,上传文件按钮样式是不是和 element ui 的样式冲突了,要怎么弄啊 😂 。
    QQ 截图 20200106095935.png

    已解决。
    zhaoyangkun 1
    @zhaoyangkun 再 reset 回去?
    Vanessa
    @Vanessa element ui 表单元素默认有行高值,我在 vditor 组件中把行高值覆盖掉就可以了。
    zhaoyangkun
  • 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
  • zhaoyangkun 2 评论

    @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 好的,我试试
    zhaoyangkun
  • zhaoyangkun 2 评论

    @Vanessa V 姐,我想在 HTML 页面中使用 vditor 默认的 Markdown 样式该怎么弄啊 😂 ?

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

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

    这个目前仅支持 Markdown 模式,wysiwyg 还不支持,可以关注 https://github.com/Vanessa219/vditor/issues/134 issue
    Vanessa
  • alexmh

    谢谢 @Vanessa

  • alexmh 2 评论

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

    那个目前是后台解析生成的,可关注 https://github.com/Vanessa219/vditor/issues/121
    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 赞同
  • zhaoyangkun 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
  • Vanessa

    可以的。

    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
  • Vanessa

    我这里使用 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>
    
    
  • Vanessa

    稍等我再看看,社区的编辑器也有人报这个问题。

  • 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 回复
  • Vanessa
    1. 是在什么情况大纲下没有刷新,我这里重现不了
    2. 请关注 https://github.com/Vanessa219/vditor/issues/343
    1 回复
  • inktear 1 评论

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

    Vanessa
  • LGSKOKO

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

    1 回复
  • Vanessa

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

  • Blackman99

    优秀

  • 547176052 1 评论

    image.png

    ant-design-pro 项目引入报错

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

    这个怎么去掉

    image.png

  • darren 1

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

  • ldan2012 1 评论

    @Vanessa image.png

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

    用 IDE
    Vanessa
  • ldan2012

    image.png

请输入回帖内容 ...