😁 基本使用示例
目前一直在迭代开发,所以尽量安装最新版本。发布日志请前往: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
应该是字符类型,例:html
、body
、#id
、.class
。
🎛 Web Component 中使用
完整的示例参考源码中提供的示例项目
下面是注意事项
- 内部的图片放大查看无效,需要自行实现!!!
- 不能默认的使用 CDN 引用依赖库,参考[自行引入扩展库]!!!
🥂 扩展功能
这里包含了一些编辑器api
的使用示范
🥶 自定义快捷键
-
内置的快捷键配置的源码:commands.ts,它们作为扩展项被添加到了
codemirror
。 -
想要替换、删除快捷键的基本原理是找到对应的扩展,然后遍历这个快捷键配置的数组,找到并处理它。
-
事实上,
config
中codeMirrorExtensions
的第二入参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>
🍡 预览主题
内置了default
、github
、vuepress
、mk-cute
、smart-blue
、cyanosis
6 种主题,在一些直接预览文档内容时使用。并且支持在线切换(修改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>
-
自定义
- 先以
xxx-theme
为类名,定义你的主题css
,xxx 是主题名称,具体的内容参考markdown-theme
xxx.css
css.xxx-theme code { color: red; }
- 全局引入
jsimport 'xxx.css';
- 设置
previewTheme
为 xxx
vue<template> <MdEditor previewTheme="xxx" /> </template>
- 先以
🎄 代码主题
内置了atom
、a11y
、github
、gradient
、kimbie
、paraiso
、qtcreator
、stackoverflow
代码主题,均来至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>
-
自定义
- 找到你喜欢的代码主题,最好支持暗夜模式
jsimport { 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', }, }, }, }, });
你可以通过将
css
的key
设置为内置名称来覆盖内置的链接。- 设置
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,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;
}
🙍🏻♂️ 自行引入扩展库
这里给出一个完全不使用外部链接,全部自行引入的示例:
- 安装依赖
shell
yarn add screenfull katex cropperjs mermaid highlight.js prettier
- 配置到编辑器
我们建议你在项目入口配置,例如 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()];
},
});
🏄🏻♂️ 新窗口打开链接
- 安装额外的扩展
shell
yarn add markdown-it-link-attributes
- 将扩展添加到编译器中
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;
},
});