😁 基本使用示例

目前一直在迭代开发,所以尽量安装最新版本。发布日志请前往:releases

🤖 NPM 安装

shell shell 复制代码
npm install md-editor-v3
yarn add md-editor-v3

当使用服务端渲染时,请务必设置editorId为固定值。

5.0 开始,没有该限制了。

🤓 CDN 链接

通过直接链接生产版本来使用,下面是一个小例子:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>全局引用</title>
    <link
      href="https://unpkg.com/md-editor-v3@5.0.2/lib/style.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="md-editor-v3">
      <md-editor-v3 v-model="text" />
    </div>
    <script src="https://unpkg.com/vue@3.5.12/dist/vue.global.prod.js"></script>
    <script src="https://unpkg.com/md-editor-v3@5.0.2/lib/umd/index.js"></script>
    <script>
      const App = {
        data() {
          return {
            text: 'Hello Editor!!',
          };
        },
      };
      Vue.createApp(App).use(MdEditorV3.MdEditor).mount('#md-editor-v3');
    </script>
  </body>
</html>

🥱 Setup 模板

vue 复制代码
<template>
  <MdEditor v-model="text" />
</template>

<script setup>
import { ref } from 'vue';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

const text = ref('Hello Editor!');
</script>

🤗 Jsx 模板

js 复制代码
import { defineComponent, ref } from 'vue';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

export default defineComponent({
  name: 'MdEditor',
  setup() {
    const text = ref('');
    const onChange = (v: string) => (text.value = v);

    return () => <MdEditor modelValue={text.value} onChange={onChange} />;
  },
});

📖 仅预览

vue 复制代码
<template>
  <MdPreview :id="id" :modelValue="text" />
  <MdCatalog :editorId="id" :scrollElement="scrollElement" />
</template>

<script setup>
import { ref } from 'vue';
import { MdPreview, MdCatalog } from 'md-editor-v3';
// preview.css相比style.css少了编辑器那部分样式
import 'md-editor-v3/lib/preview.css';

const id = 'preview-only';
const text = ref('# Hello Editor');
const scrollElement = document.documentElement;
</script>

当使用服务端渲染时,scrollElement应该是字符类型,例:htmlbody#id.class

🎛 Web Component 中使用

完整的示例参考源码中提供的示例项目

下面是注意事项

  1. 内部的图片放大查看无效,需要自行实现!!!
  2. 不能默认的使用 CDN 引用依赖库,参考[自行引入扩展库]!!!

🥂 扩展功能

这里包含了一些编辑器api的使用示范

🥶 自定义快捷键

  • 内置的快捷键配置的源码:commands.ts,它们作为扩展项被添加到了codemirror

  • 想要替换、删除快捷键的基本原理是找到对应的扩展,然后遍历这个快捷键配置的数组,找到并处理它。

  • 事实上,configcodeMirrorExtensions的第二入参extensions是一个数组,它的第一项就是快捷键扩展,第三入参就是默认的快捷键配置。

💅 修改快捷键

Ctrl-b修改为Ctrl-m

js 复制代码
import { config } from 'md-editor-v3';
import { keymap } from '@codemirror/view';

config({
  // [keymap, minimalSetup, markdown, EditorView.lineWrapping, EditorView.updateListener, EditorView.domEventHandlers, oneDark??oneLight]
  codeMirrorExtensions(theme, extensions, mdEditorCommands) {
    const newExtensions = [...extensions];
    // 1. 先把默认的快捷键扩展移除
    newExtensions.shift();

    // 2. 参考快捷键配置的源码,找到CtrlB的配置项在mdEditorCommands中的位置
    const CtrlB = mdEditorCommands[0];

    // 3. 配置codemirror快捷键的文档
    // https://codemirror.net/docs/ref/#commands
    const CtrlM = {
      // 这里我们需要CtrlB默认触发执行的run方法,如果是新增快捷键等,就需要自行处理触发逻辑
      ...CtrlB,
      key: 'Ctrl-m',
      mac: 'Cmd-m',
    };

    // 4. 把修改后的快捷键放到待构建扩展的数组中
    const newMdEditorCommands = [
      CtrlM,
      ...mdEditorCommands.filter((i) => i.key !== 'Ctrl-b'),
    ];

    newExtensions.push(keymap.of(newMdEditorCommands));

    return newExtensions;
  },
});

✂️ 删除快捷键

禁用所有快捷键

js 复制代码
import { config } from 'md-editor-v3';

config({
  // [keymap, minimalSetup, markdown, EditorView.lineWrapping, EditorView.updateListener, EditorView.domEventHandlers, oneDark??oneLight]
  codeMirrorExtensions(theme, extensions) {
    const newExtensions = [...extensions];
    // 1. 把默认的快捷键扩展移除
    newExtensions.shift();

    // 2. 返回扩展列表即可
    return newExtensions;
  },
});

💉 新增快捷键

如果涉及到向编辑框插入内容,这是需要借助组件实例上绑定的insert方法,参考手动向文本框插入内容

如果不是在编辑器所在的组件中使用config,这是无法拿到编辑器组件实例,这时,你可能需要借助event-bus

示例实现Ctrl+m向编辑框插入标记模块(==mark==)

index.ts

js 复制代码
import { config } from 'md-editor-v3';
import { keymap, KeyBinding } from '@codemirror/view';
// 假设你使用了EventBus
import bus from '@/utils/event-bus';

config({
  // [keymap, minimalSetup, markdown, EditorView.lineWrapping, EditorView.updateListener, EditorView.domEventHandlers, oneDark??oneLight]
  codeMirrorExtensions(theme, extensions, mdEditorCommands) {
    const newExtensions = [...extensions];
    // 1. 先把默认的快捷键扩展移除
    newExtensions.shift();

    // 2. 创建一个新的快捷键配置,参考https://codemirror.net/docs/ref/#commands
    const CtrlM: KeyBinding = {
      key: 'Ctrl-m',
      mac: 'Cmd-m',
      run: () => {
        bus.emit('insertMarkBlock');
        return true;
      },
    };

    // 4. 把新的快捷键添加到数组中
    const newMdEditorCommands = [...mdEditorCommands, CtrlM];

    newExtensions.push(keymap.of(newMdEditorCommands));

    return newExtensions;
  },
});

接下来在编辑器所在组件监听insertMarkBlock这个事件

index.vue

vue 复制代码
<template>
  <MdEditor ref="mdEditorRef" v-model="text" />
</template>

<script setup lang="ts">
import { MdEditor } from 'md-editor-v3';
import type { ExposeParam } from 'md-editor-v3';
import { ref, onMounted } from 'vue';
// 假设你使用了EventBus
import bus from '@/utils/event-bus';

const text = ref<string>('## md-editor-v3\n\n');

const mdEditorRef = ref<ExposeParam>();

onMounted(() => {
  bus.on('insertMarkBlock', () => {
    mdEditorRef.value?.insert((selectedText) => {
      return {
        targetValue: `==${selectedText}==`,
        select: true,
        deviationStart: 2,
        deviationEnd: -2,
      };
    });
  });
});
</script>

附:EventBus最简单实现

ts 复制代码
/* eslint-disable @typescript-eslint/ban-types */
class EventBus {
  private events: Map<string, Function[]>;

  constructor() {
    this.events = new Map();
  }

  on(eventName: string, fn: Function) {
    if (!eventName) {
      console.error('无效的事件名称');
      return false;
    }

    if (!(fn instanceof Function)) {
      console.error('无效的回调方法');
      return false;
    }

    const fns = this.events.get(eventName) || [];
    fns.push(fn);
    this.events.set(eventName, fns);
  }

  emit(eventName: string, ...args: any[]) {
    this.events.get(eventName)?.forEach((fn) => {
      fn(args);
    });
  }
}

export default new EventBus();

🍦 主题切换

主题分为了编辑器主题(theme,称为全局主题)、预览内容主题(previewTheme)和块级代码主题(codeTheme),他们都支持响应式更新,而非只能预设。

🍧 编辑器主题

支持默认和暗夜模式两种

vue 复制代码
<template>
  <MdEditor v-model="state.text" :theme="state.theme" />
</template>

<script setup>
import { reactive } from 'vue';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

const state = reactive({
  text: '',
  theme: 'dark',
});
</script>

🍡 预览主题

内置了defaultgithubvuepressmk-cutesmart-bluecyanosis6 种主题,在一些直接预览文档内容时使用。并且支持在线切换(修改previewTheme即可)和自行扩展。

  • 使用

    vue 复制代码
    <template>
      <MdEditor v-model="state.text" :previewTheme="state.theme" />
    </template>
    
    <script setup>
    import { reactive } from 'vue';
    import { MdEditor } from 'md-editor-v3';
    import 'md-editor-v3/lib/style.css';
    
    const state = reactive({
      text: '',
      theme: 'cyanosis',
    });
    </script>
  • 自定义

    1. 先以xxx-theme为类名,定义你的主题css,xxx 是主题名称,具体的内容参考markdown-theme

    xxx.css

    css 复制代码
    .xxx-theme code {
      color: red;
    }
    1. 全局引入
    js 复制代码
    import 'xxx.css';
    1. 设置previewTheme为 xxx
    vue 复制代码
    <template>
      <MdEditor previewTheme="xxx" />
    </template>

🎄 代码主题

内置了atoma11ygithubgradientkimbieparaisoqtcreatorstackoverflow代码主题,均来至highlight.js

  • 使用

    vue 复制代码
    <template>
      <MdEditor v-model="state.text" :codeTheme="state.theme" />
    </template>
    
    <script setup>
    import { reactive } from 'vue';
    import { MdEditor } from 'md-editor-v3';
    import 'md-editor-v3/lib/style.css';
    
    const state = reactive({
      text: '',
      theme: 'atom',
    });
    </script>
  • 自定义

    1. 找到你喜欢的代码主题,最好支持暗夜模式
    js 复制代码
    import { config } from 'md-editor-v3';
    
    config({
      editorExtensions: {
        highlight: {
          css: {
            xxxxx: {
              light:
                'https://unpkg.com/highlight.js@11.2.0/styles/xxxxx-light.css',
              dark: 'https://unpkg.com/highlight.js@11.2.0/styles/xxxxx-dark.css',
            },
            yyyyy: {
              light:
                'https://unpkg.com/highlight.js@11.2.0/styles/xxxxx-light.css',
              dark: 'https://unpkg.com/highlight.js@11.2.0/styles/xxxxx-dark.css',
            },
          },
        },
      },
    });

    你可以通过将csskey设置为内置名称来覆盖内置的链接。

    1. 设置codeTheme
    vue 复制代码
    <template>
      <MdEditor codeTheme="xxxxx" />
    </template>

🛠 扩展库替换

highlight、prettier、cropper、screenfull 均使用外链引入,在无外网的时候,部分可将项目中已安装的依赖传入,也可以使用下载好的引用。

screenfull 的例子:

⚰️ 内置实例

vue 复制代码
<template>
  <MdEditor v-model="text" />
</template>

<script setup>
import { ref } from 'vue';
// 引用screenfull
import screenfull from 'screenfull';
import { MdEditor, config } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

config({
  editorExtensions: {
    screenfull: {
      instance: screenfull,
    },
  },
});

const text = ref('');
</script>

📡 内网链接

对应的 js 文件可以去unpkg.com,直接找到对应的文件下载即可。

vue 复制代码
<template>
  <MdEditor v-model="text" />
</template>

<script setup>
import { ref } from 'vue';
import { MdEditor, config } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

config({
  editorExtensions: {
    screenfull: {
      js: 'https://localhost:8090/screenfull@5.2.0/index.js',
    },
  },
});

const text = ref('');
</script>

📷 图片上传

默认可以选择多张图片,支持截图粘贴板上传图片,支持复制网页图片粘贴上传。

注意:粘贴板上传时,如果是网页上的 gif 图,无法正确上传为 gif 格式!请保存本地后再手动上传。

vue 复制代码
<template>
  <MdEditor v-model="text" @onUploadImg="onUploadImg" />
</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

const text = ref('# Hello Editor');

const onUploadImg = async (files, callback) => {
  const res = await Promise.all(
    files.map((file) => {
      return new Promise((rev, rej) => {
        const form = new FormData();
        form.append('file', file);

        axios
          .post('/api/img/upload', form, {
            headers: {
              'Content-Type': 'multipart/form-data',
            },
          })
          .then((res) => rev(res))
          .catch((error) => rej(error));
      });
    })
  );

  callback(res.map((item) => item.data.url));
};
</script>

🏳️‍🌈 语言扩展与替换

vue 复制代码
<template>
  <MdEditor v-model="state.text" :language="state.language" />
</template>

<script setup>
import { reactive } from 'vue';
import { MdEditor, config } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

config({
  editorConfig: {
    languageUserDefined: {
      'my-lang': {
        toolbarTips: {
          bold: '加粗',
          underline: '下划线',
          italic: '斜体',
          strikeThrough: '删除线',
          title: '标题',
          sub: '下标',
          sup: '上标',
          quote: '引用',
          unorderedList: '无序列表',
          orderedList: '有序列表',
          task: '任务列表',
          codeRow: '行内代码',
          code: '块级代码',
          link: '链接',
          image: '图片',
          table: '表格',
          mermaid: 'mermaid图',
          katex: 'katex公式',
          revoke: '后退',
          next: '前进',
          save: '保存',
          prettier: '美化',
          pageFullscreen: '浏览器全屏',
          fullscreen: '屏幕全屏',
          preview: '预览',
          htmlPreview: 'html代码预览',
          catalog: '目录',
          github: '源码地址',
        },
        titleItem: {
          h1: '一级标题',
          h2: '二级标题',
          h3: '三级标题',
          h4: '四级标题',
          h5: '五级标题',
          h6: '六级标题',
        },
        imgTitleItem: {
          link: '添加链接',
          upload: '上传图片',
          clip2upload: '裁剪上传',
        },
        linkModalTips: {
          linkTitle: '添加链接',
          imageTitle: '添加图片',
          descLabel: '链接描述:',
          descLabelPlaceHolder: '请输入描述...',
          urlLabel: '链接地址:',
          urlLabelPlaceHolder: '请输入链接...',
          buttonOK: '确定',
        },
        clipModalTips: {
          title: '裁剪图片上传',
          buttonUpload: '上传',
        },
        copyCode: {
          text: '复制代码',
          successTips: '已复制!',
          failTips: '复制失败!',
        },
        mermaid: {
          flow: '流程图',
          sequence: '时序图',
          gantt: '甘特图',
          class: '类图',
          state: '状态图',
          pie: '饼图',
          relationship: '关系图',
          journey: '旅程图',
        },
        katex: {
          inline: '行内公式',
          block: '块级公式',
        },
        footer: {
          markdownTotal: '字数',
          scrollAuto: '同步滚动',
        },
      },
    },
  },
});

const state = reactive({
  text: '',
  // 定义语言名称
  language: 'my-lang',
});
</script>

你也可以使用现成的扩展语言:md-editor-extension。使用及贡献方式见扩展库文档~

📄 目录获取与展示

  • 获取

    vue 复制代码
    <template>
      <MdEditor v-model="text" @onGetCatalog="onGetCatalog" />
    </template>
    
    <script setup>
    import { reactive } from 'vue';
    import { MdEditor } from 'md-editor-v3';
    import 'md-editor-v3/lib/style.css';
    
    const state = reactive({
      text: '',
      catalogList: [],
    });
    
    const onGetCatalog = (list) => {
      state.catalogList = list;
    };
    </script>
  • 展示

    使用内置MdCatalog组件

    vue 复制代码
    <template>
      <MdPreview :modelValue="state.text" :id="state.id" :theme="state.theme" />
      <MdCatalog
        :editorId="state.id"
        :scrollElement="scrollElement"
        :theme="state.theme"
      />
    </template>
    
    <script setup>
    import { reactive } from 'vue';
    import { MdPreview, MdCatalog } from 'md-editor-v3';
    import 'md-editor-v3/lib/preview.css';
    
    const state = reactive({
      theme: 'dark',
      text: '标题',
      id: 'my-editor',
    });
    
    const scrollElement = document.documentElement;
    </script>

🪚 调整工具栏

v1.6.0开始,支持调整工具栏内容顺序和分割符了。

vue 复制代码
<template>
  <MdEditor v-model="text" :toolbars="toolbars" />
</template>

<script setup>
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

const toolbars = ['italic', 'underline', '-', 'bold', '=', 'github'];
</script>

💪 自定义工具栏

这里包含了mark标记扩展普通工具栏和emoji扩展下拉工具栏的类型

可运行源码参考本文档docs

标记及Emoji预览

更多 emoji,https://getemoji.com/

🧙‍♂️ 调整编辑器样式

2.x 使用 css 变量定义了大部分内容:

less 复制代码
.css-vars(@isDark) {
  --md-color: if(@isDark, #999, #222);
  --md-hover-color: if(@isDark, #bbb, #000);
  --md-bk-color: if(@isDark, #000, #fff);
  --md-bk-color-outstand: if(@isDark, #333, #f2f2f2);
  --md-bk-hover-color: if(@isDark, #1b1a1a, #f5f7fa);
  --md-border-color: if(@isDark, #2d2d2d, #e6e6e6);
  --md-border-hover-color: if(@isDark, #636262, #b9b9b9);
  --md-border-active-color: if(@isDark, #777, #999);
  --md-modal-mask: #00000073;
  --md-scrollbar-bg-color: if(@isDark, #0f0f0f, #e2e2e2);
  --md-scrollbar-thumb-color: if(@isDark, #2d2d2d, #0000004d);
  --md-scrollbar-thumb-hover-color: if(@isDark, #3a3a3a, #00000059);
  --md-scrollbar-thumb-active-color: if(@isDark, #3a3a3a, #00000061);
}

.md-editor {
  .css-vars(false);
}

.md-editor-dark {
  .css-vars(true);
}

只需要调整对应的 css 变量,比如调整暗夜模式下的背景:

css 复制代码
.md-editor-dark {
  --md-bk-color: #333 !important;
}

🙍🏻‍♂️ 自行引入扩展库

这里给出一个完全不使用外部链接,全部自行引入的示例:

  1. 安装依赖
shell 复制代码
yarn add screenfull katex cropperjs mermaid highlight.js prettier
  1. 配置到编辑器

我们建议你在项目入口配置,例如 vite 创建的项目中的 main.js。不要在组件中去调用 config

main.js

js 复制代码
import { config } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

import screenfull from 'screenfull';

import katex from 'katex';
import 'katex/dist/katex.min.css';

import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';

import mermaid from 'mermaid';

import highlight from 'highlight.js';
import 'highlight.js/styles/atom-one-dark.css';

// <3.0
// import prettier from 'prettier';
// import parserMarkdown from 'prettier/parser-markdown';
// >=3.0
import * as prettier from 'prettier';
import parserMarkdown from 'prettier/plugins/markdown';

config({
  editorExtensions: {
    prettier: {
      prettierInstance: prettier,
      parserMarkdownInstance: parserMarkdown,
    },
    highlight: {
      instance: highlight,
    },
    screenfull: {
      instance: screenfull,
    },
    katex: {
      instance: katex,
    },
    cropper: {
      instance: Cropper,
    },
    mermaid: {
      instance: mermaid,
    },
  },
});

App.vue

vue 复制代码
<template>
  <MdEditor v-model="text" />
</template>

<script setup>
import { ref } from 'vue';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

const text = ref('');
</script>

注意:highlight 的样式自行引入后,将不支持切换代码样式。

🔒 编译时防范 XSS

5.0 版本将内置的 XSS 扩展导出了,不再默认添加, 导出的 XSS 扩展在默认白名单的基础上,增加了部分标签和属性:

json 复制代码
{
  "img": ["class"],
  // 支持任务列表
  "input": ["class", "disabled", "type", "checked"],
  // 主要支持youtobe、腾讯视频、哔哩哔哩等内嵌视频代码
  "iframe": [
    "class",
    "width",
    "height",
    "src",
    "title",
    "border",
    "frameborder",
    "framespacing",
    "allow",
    "allowfullscreen"
  ]
}

🔒 添加 xss 扩展

js 复制代码
import { config, XSSPlugin } from 'md-editor-v3';

config({
  markdownItPlugins(plugins) {
    return [
      ...plugins,
      {
        type: 'xss',
        plugin: XSSPlugin,
        options: {},
      },
    ];
  },
});

🔏 修改 xss 配置

我们添加一个允许图片加载失败的事件

js 复制代码
import { config, XSSPlugin } from 'md-editor-v3';
// import { getDefaultWhiteList } from 'xss';

config({
  markdownItPlugins(plugins) {
    return [
      ...plugins,
      {
        type: 'xss',
        plugin: XSSPlugin,
        options: {
          // 方式一:自行扩展全部
          // xss() {
          //   return {
          //     whiteList: Object.assign({}, getDefaultWhiteList(), {
          //       // 如果你需要使用任务列表,请保留这项配置
          //       img: ['class'],
          //       input: ['class', 'disabled', 'type', 'checked'],
          //       // 如果你需要使用嵌入视频代码,请保留这项配置
          //       iframe: [
          //         'class',
          //         'width',
          //         'height',
          //         'src',
          //         'title',
          //         'border',
          //         'frameborder',
          //         'framespacing',
          //         'allow',
          //         'allowfullscreen'
          //       ],
          //       img: ['onerror']
          //     })
          //   };
          // },
          // 方式二:在默认白名单的基础上新增。^4.15.6
          extendedWhiteList: {
            img: ['onerror'],
          },
        },
      },
    ];
  },
});

更新详细配置参考 js-xss

🔒 编译后防范 XSS

通过sanitize属性,自行处理不安全的 html 内容。例如:使用sanitize-html处理

shell 复制代码
yarn add sanitize-html
vue 复制代码
<template>
  <MdEditor :sanitize="sanitize" />
</template>

<script setup>
import sanitizeHtml from 'sanitize-html';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

const sanitize = (html) => {
  return sanitizeHtml(html);
};
</script>

更详细的实现可以参考本文档的源码!

🗂 折叠文档内容

js 复制代码
import { config } from 'md-editor-v3';
import { foldGutter } from '@codemirror/language';
import { lineNumbers } from '@codemirror/view';

config({
  codeMirrorExtensions(_theme, extensions) {
    return [...extensions, lineNumbers(), foldGutter()];
  },
});

🏄🏻‍♂️ 新窗口打开链接

  1. 安装额外的扩展
shell 复制代码
yarn add markdown-it-link-attributes
  1. 将扩展添加到编译器中
js 复制代码
import { config } from 'md-editor-v3';
import LinkAttr from 'markdown-it-link-attributes';
// import Anchor from 'markdown-it-anchor';

config({
  markdownItPlugins(plugins) {
    return [
      ...plugins,
      {
        type: 'linkAttr',
        plugin: LinkAttr,
        options: {
          matcher(href: string) {
            // 如果使用了markdown-it-anchor
            // 应该忽略标题头部的锚点链接
            return !href.startsWith('#');
          },
          attrs: {
            target: '_blank',
          },
        },
      },
      // {
      //   type: 'anchor',
      //   plugin: Anchor,
      //   options: {
      //     permalink: Anchor.permalink.headerLink(),
      //     slugify(s: string) {
      //       return s;
      //     }
      //   }
      // }
    ];
  },
});

☑️ 可切换状态的任务列表

js 复制代码
import { config } from 'md-editor-v3';
config({
  markdownItPlugins(plugins, { editorId }) {
    return plugins.map((item) => {
      if (item.type === 'taskList') {
        return {
          ...item,
          options: {
            ...item.options,
            enabled: true,
            // 如果只是想对某个编辑器开启这个功能
            // enabled: editorId === 'myId'
          },
        };
      }
      return item;
    });
  },
});
vue 复制代码
<MdEditor id="myId" v-model="text" />

🎳 协同办公

安装yjs及相关库

shell 复制代码
npm i yjs y-codemirror.next y-websocket

在 main.js 中添加 yjs 扩展:

js 复制代码
import { config } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

import * as Y from 'yjs';
import * as random from 'lib0/random';
import { yCollab } from 'y-codemirror.next';
import { WebsocketProvider } from 'y-websocket';

const usercolors = [
  { color: '#30bced', light: '#30bced33' },
  { color: '#6eeb83', light: '#6eeb8333' },
  { color: '#ffbc42', light: '#ffbc4233' },
  { color: '#ecd444', light: '#ecd44433' },
  { color: '#ee6352', light: '#ee635233' },
  { color: '#9ac2c9', light: '#9ac2c933' },
  { color: '#8acb88', light: '#8acb8833' },
  { color: '#1be7ff', light: '#1be7ff33' },
];

// select a random color for this user
const userColor = usercolors[random.uint32() % usercolors.length];

const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
  // Start a websocket server quickly: https://github.com/yjs/y-websocket?tab=readme-ov-file#start-a-y-websocket-server
  'ws://127.0.0.1:1234',
  'md-editor-v3-room',
  ydoc
);
const ytext = ydoc.getText('module-name');

const undoManager = new Y.UndoManager(ytext);

provider.awareness.setLocalStateField('user', {
  name: 'Anonymous ' + Math.floor(Math.random() * 100),
  color: userColor.color,
  colorLight: userColor.light,
});

config({
  codeMirrorExtensions(_theme, extensions) {
    return [...extensions, yCollab(ytext, provider.awareness, { undoManager })];
  },
});

如果只想在某一个编辑器中使用,尝试通过editorId进行区别(^4.20.0):

js 复制代码
config({
  codeMirrorExtensions(_theme, extensions, _keyBindings, { editorId }) {
    return editorId === 'myId'
      ? [...extensions, yCollab(ytext, provider.awareness, { undoManager })]
      : extensions;
  },
});

🧻 编辑此页面

demo-zh-CN