Skip to main content

您已有的文本区域上的工具栏、实时预览和所见即所得

最后更新:

This translation is older than the English page and may be missing recent changes.

Read the English version

一个 Markdown 编辑器,可以增强您的文本区域而不是替换它,因此表单提交、验证和必填字段可以继续工作。所见即所得和纯 Markdown 模式、实时预览、查找和替换、RTL 支持、深色模式。可与 Django、Laravel、Rails、Node.js、PHP 和任何堆栈独立工作。

不需要 Frutjam 和 Tailwind。 样式是捆绑的,因此编辑器可以放入任何项目中。

安装

NPM(捆绑器:Vite、webpack、Rollup 等)

bash
npm install markdown-text-editor
javascript
1
2
import MarkdownEditor from 'markdown-text-editor';
new MarkdownEditor('#markdown-editor');

TypeScript 定义随软件包一起提供,因此选项、工具栏条目和变量形状都会被检查并自动完成,无需额外安装。

这些类型也是可导入的,只要您的选项不是在调用站点内联编写的,这一点就很重要。 TypeScript 仅检查直接传递的对象文字上的未知键,因此保存在其自己的变量中的配置会默默忽略拼写错误的键。对其进行注释会将检查放回原处。

typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import MarkdownEditor, { type MarkdownEditorOptions, type ToolbarEntry } from 'markdown-text-editor';

// Without the annotation this compiles, the editor renders with the default
// toolbar, and nothing tells you why.
const config: MarkdownEditorOptions = {
    placeholder: 'Write...',
    toolbr: ['bold', 'preview'],   // Error: did you mean to write 'toolbar'?
};

// Building a toolbar conditionally
const toolbar: ToolbarEntry[] = ['bold', 'italic'];
if (canUpload) toolbar.push({ image: { fileInput: { uploadUrl: '/api/upload' } } });
toolbar.push('preview');

这同样适用于包装组件,其中 MarkdownEditorOptions 是你转发的而不是 any,所以无论谁使用你的包装器都会进行检查。

typescript
1
2
3
4
interface EditorProps {
    value: string;
    options?: MarkdownEditorOptions;
}

CDN:ES模块

html
1
2
3
4
<script type="module">
  import MarkdownEditor from 'https://cdn.jsdelivr.net/npm/markdown-text-editor/+esm';
  new MarkdownEditor('#markdown-editor');
</script>

CDN:全局脚本标签(IIFE)

无需导入:MarkdownEditor 可自动作为全局变量使用。

html
1
2
3
4
5
6
7
8
9
<form action="/api/save" method="POST">
  <textarea id="markdown-editor" name="content"># Hello World</textarea>
  <button type="submit">Save Content</button>
</form>

<script src="https://cdn.jsdelivr.net/npm/markdown-text-editor"></script>
<script>
  new MarkdownEditor('#markdown-editor');
</script>

Markdown 编辑器演示

快速入门

传递一个选项对象来自定义编辑器。所有选项都是可选的:省略任何选项即可使用默认值。

javascript
1
2
3
4
const editor = new MarkdownEditor('#markdown-editor', {
  placeholder: 'Write your markdown...',
  toolbar: ['heading', 'bold', 'italic', 'strikethrough', 'ul', 'ol', 'checklist', 'blockquote', 'link', 'preview'],
});

理念:“本土第一”

大多数编辑器都打破了标准的网络工作流程。 MarkdownEditor 拥抱它。因为它直接位于 <textarea>,您不需要学习处理数据的新方法。

  • 无需数据绑定: 适用于 <form method="POST"> 开箱即用
  • 标准访问: 使用 document.getElementById('editor').value 就像普通输入一样。
  • 与后端无关: 可与任何后端(Python、Node.js、PHP 等)一起使用,就像普通表单字段一样

MarkdownEditor 与 EasyMDE / SimpleMDE

大多数 JavaScript Markdown 编辑器(EasyMDE、SimpleMDE、基于 CodeMirror 的编辑器)隐藏您的 <textarea> 并编辑副本,在提交表单时写回内容。这一直有效,直到其他东西需要该值为止: required 浏览器无法聚焦的字段会完全阻止提交,并且 .value 或者 FormData 在提交之前读取会返回一个空字符串,这会破坏 htmx、Turbo、自动保存和未保存更改防护。 MarkdownEditor 是不同的。 它对您已有的文本区域进行样式设置,并将其保留为您输入的字段,因此该值在任何时候都是正确的。

特征Markdown编辑器EasyMDE / SimpleMDE
保留本机文本区域✅❌ 隐藏,编辑为副本
与 required 领域✅❌ 浏览器阻止提交
.value 提交前更正✅❌ 表单提交之前为空
连载于 FormData, htmx, 涡轮增压✅❌ 需要编辑器自己的API
所见即所得混合模式✅❌
内置查找和替换✅❌
RTL 支持✅ 内置通过 CodeMirror 的 direction 选项
内联事件处理程序 (CSP)✅ 无一些
深色模式/主题✅有限的
捆绑包大小,gzip 压缩53 KB107 KB(JS + CSS)

框架整合

因为 MarkdownEditor 保留了原生 <textarea>,它与每个后端框架集成,无需任何额外代码。您的服务器接收 Markdown 内容,就像从任何标准表单字段接收一样。对于 React 和 Vue,它需要几行,在本节末尾介绍。

姜戈

添加一个 class 到您的文本区域小部件并初始化编辑器: request.POST['content'] 无需额外步骤即可工作。

python
1
2
3
4
5
6
7
8
# forms.py
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['content']
        widgets = {
            'content': forms.Textarea(attrs={'class': 'markdown-editor'}),
        }
javascript
1
2
new MarkdownEditor('.markdown-editor');
// request.POST['content'] contains the markdown on submit

拉维尔

使用 f.text_area 有一个类: $request->input('content') 直接接收markdown。

html
1
2
<textarea name="content" class="markdown-editor">{{ old('content') }}</textarea>
<script>new MarkdownEditor('.markdown-editor');</script>

红宝石 on Rails

与 form_with 开箱即用: params[:content] 包含降价。对于涡轮驱动器,请使用 turbo:load 而不是 DOMContentLoaded.

javascript
1
2
3
4
5
6
document.addEventListener('turbo:load', () => {
    document.querySelectorAll('.markdown-editor:not([data-mde-init])').forEach(el => {
        el.setAttribute('data-mde-init', 'true');
        new MarkdownEditor(el);
    });
});

Node.js / Express

req.body.content 接收表单提交时的降价:没有同步步骤,没有自定义提取。

html
1
2
3
<textarea name="content" class="markdown-editor"></textarea>
<script src="https://cdn.jsdelivr.net/npm/markdown-text-editor"></script>
<script>new MarkdownEditor('.markdown-editor');</script>

PHP

$_POST['content'] 与任何标准文本区域完全相同:将其放入,您现有的表单处理需要零更改。

html
1
2
3
4
5
6
<form method="POST" action="save.php">
  <textarea name="content" class="markdown-editor"></textarea>
  <button type="submit">Save</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/markdown-text-editor"></script>
<script>new MarkdownEditor('.markdown-editor');</script>

反应

在效果中创建编辑器并在卸载时销毁它。使用 defaultValue 而不是 value:编辑器直接写入文本区域,因此受控绑定将覆盖用户键入的内容。

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { useEffect, useRef } from 'react';
import MarkdownEditor from 'markdown-text-editor';

function MarkdownField({ name, defaultValue = '', onChange }) {
    const ref = useRef(null);

    useEffect(() => {
        const editor = new MarkdownEditor(ref.current, { onChange });
        return () => editor.destroy();
    }, []);

    return &lt;textarea ref={ref} name={name} defaultValue={defaultValue} /&gt;;
}

返回 editor.destroy() from theeffect 还涵盖了 StrictMode,它在开发过程中运行两次效果,否则会在一个文本区域上留下两个编辑器。空依赖数组是故意的:创建编辑器时会读取一次选项,因此重新运行效果会拆除它并在每次更改时重建它。

维埃

同样的想法。设置一次初始值 onMounted 并且不绑定 :value,或通过发送回的每个击键 v-model 会覆盖文本区域。

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
&lt;script setup&gt;
import { ref, onMounted, onBeforeUnmount } from 'vue';
import MarkdownEditor from 'markdown-text-editor';

const el = ref(null);
const props = defineProps({ modelValue: { type: String, default: '' } });
const emit = defineEmits(['update:modelValue']);
let editor = null;

onMounted(() => {
    el.value.value = props.modelValue;
    editor = new MarkdownEditor(el.value, {
        onChange: value =&gt; emit('update:modelValue', value)
    });
});

onBeforeUnmount(() =&gt; editor?.destroy());
&lt;/script&gt;

&lt;template&gt;&lt;textarea ref="el"&gt;&lt;/textarea&gt;&lt;/template&gt;

用作 <MarkdownField v-model="content" />。在简单的形式中,您可以跳过 v-model 与任何其他框架一样,完全并在提交时从文本区域读取值。

配置

您可以通过传递一个来完全自定义编辑器的行为和界面 options 目的。如果省略选项,则使用 默认 值。

构建编辑器时,选项将被读取一次。之后更改它们没有任何效果:调用 destroy() 并创建一个新的编辑器。

财产类型默认目的
modestring'plain'设置初始视图。使用混合以获得所见即所得的体验,或使用普通的原始语法。
placeholderstring'Write...'编辑器为空时显示的文本。
toolbararray[...]定义显示哪些工具以及显示顺序。
footer假| 对象全部可见控制编辑器下方显示的状态栏。设置为 false 完全隐藏它,或者传递一个对象来切换单个统计数据。
themestringinherited显式设置编辑器主题(浅色、深色、雪莓、黑莓)。如果省略,编辑器继承 data-theme 从最近的祖先元素或 <textarea> 本身。
minHeightnumber200当内容较短时,编辑器将缩小到的最小高度(以像素为单位)。配对 maxHeight 设置自动增长范围。
maxHeightnumber500编辑器在非全屏模式下可以增长到的最大高度(以像素为单位)。一旦内容超过此高度,编辑器内就会出现滚动条。该编辑器还有一个拖动手柄,因此用户可以手动将其大小调整到超出此限制。
rendererfunctionmarked替换用于预览的 Markdown 解析器。接收 markdown 字符串并且必须返回 HTML 字符串。
sanitizerfunctionDOMPurify取代 HTML 清理程序。接收渲染的 HTML,并且必须返回安全的 HTML 来显示。
onChangefunctionundefined每次内容更改时都会触发回调:键入、工具栏操作、撤消/重做和列表延续。接收当前的 markdown 字符串作为其唯一的参数。
labelsobjectundefined编辑器自己界面的翻译,由英文字符串键入。您遗漏的任何内容都将保留为英文。

🛠 工具栏自定义

工具栏是模块化的。您可以通过修改阵列来创建最低限度的体验或功能齐全的电源套件。

可用工具

类别工具键
版式heading, bold, italic, strikethrough, blockquote
列表ul (bullet), ol (numbered), checklist
代码code (排队), codeblock (围栏块)
刀片hr (水平规则), table (表格模板)
媒体link, image
编辑undo, redo, indent, outdent
看法preview
模板{ variables: [...] }, 内联配置

工具参考

工具描述
heading打开下拉菜单以选择标题级别 H1–H6
bold启用粗体文本格式。
italic启用斜体文本格式。
strikethrough允许文本删除线。
ol(有序列表):将文本转换为编号列表格式。
ul(无序列表):将文本转换为项目符号列表。
checklist在文本中添加复选框,使其非常适合任务、待办事项列表或跟踪完成状态。
blockquote突出显示引用或强调的文本。
code将选定的文本包含在单个反引号中以实现内联代码。再次单击可删除反引号。
codeblock将选定的文本包装在三重反引号围栏代码块中。再次单击可移除栅栏。
hr插入一个 --- 水平线位于其自己的行上的光标位置。
table在光标位置插入起始 2x3 Markdown 表格模板。
image允许您通过 Markdown 语法插入图像。
link允许您向文本添加超链接。
undo撤销最后的更改。
redo重新应用上次撤消的更改。
indent增加缩进级别。
outdent降低压痕级别。
preview切换全屏并排预览。预览窗格中的复选框可单击并立即更新 Markdown 源。按 Esc 键退出全屏。如果固定标题覆盖全屏编辑器,请参阅 --mte-fullscreen-z-index.
💡实施技巧:
  • 重新排序:按钮按照您在数组中列出的确切顺序显示
  • 删除:只需省略任何键(例如 image) 从数组中完全为用户禁用该功能
  • 本机后备:如果您不提供 placeholder 在 JS 中,插件会自动使用 placeholder 来自 HTML 的属性 <textarea>

🧩 自定义工具

添加您自己的工具栏按钮,无需分叉。描述它并将其放入 toolbar 数组,在任何你想要的位置。

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
new MarkdownEditor('#markdown-editor', {
    toolbar: ['bold', 'italic', {
        custom: {
            title: 'Callout',
            icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">…</svg>',
            action(editor) {
                editor.insertText('> [!NOTE]\n> Something worth noting');
            }
        }
    }, 'preview']
});

这 title 成为工具提示和按钮的 CSS 类,因此这个样式可设置为 .callout-btn,与内置工具完全相同。

几个工具

添加任意数量。每个工具都是数组中自己的条目,因此您可以单独放置它们,并且它们可以位于内置工具之间,而不是分组在一端。

javascript
1
2
3
4
5
6
7
toolbar: [
    'bold', 'italic',
    { custom: { title: 'Callout',   icon: svg1, action: insertCallout } },
    'ul', 'ol',
    { custom: { title: 'Signature', icon: svg2, action: insertSignature } },
    'preview'
]

赋予每一个独特的 title。共享同一个标题的两个工具将共享一个 CSS 类,而没有标题的工具会退回到 .custom-btn.

选项

财产类型目的
titlestring必需的。工具提示文本和按钮 CSS 类的源。可翻译通过 labels.
actionfunction必需的。收到 (editor, event)。单击事件被传递,以便打开对话框的工具可以将其传递给 MarkdownEditor.modal.
iconstring按钮面的内联 SVG。
shortcutstring键盘快捷键,例如 Ctrl+Shift+K。添加到工具提示,并优先于使用相同键的内置工具提示。

一个工具没有 action 会跳过并显示控制台警告,而不是破坏工具栏。它不渲染任何按钮,并且它的 shortcut,如果它声明了,也没有注册。

处理内容

使用 editor.insertText() 而不是自己写入文本区域。它替换选择、恢复焦点、移动插入符、滚动、重新渲染预览并触发 onChange。撤消操作仍然有效,因为编辑是通过正常路径进行的。

javascript
1
2
3
4
5
6
7
8
action(editor) {
    const textarea = editor.usertextarea;
    const selected = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
    const wrapped = `==${selected || 'highlight'}==`;

    // (text, where the caret lands, characters to leave outside the selection)
    editor.insertText(wrapped, wrapped.length, 0);
}

快捷方式

javascript
1
2
3
4
5
6
{ custom: {
    title: 'Highlight',
    icon: '<svg>…</svg>',
    shortcut: 'Ctrl+Shift+H',
    action: (editor) => editor.insertText('====', 2, 2)
}}

工具提示变为“突出显示 (Ctrl+Shift+H)”。工具快捷方式会在内置快捷方式之前进行检查,因此自定义工具可以故意接管编辑器本来可以处理的组合。

需要自己标记的工具

下拉列表或对话框无法以声明方式描述,因此请编写一个类。延长 MarkdownEditor.Tool, 放 this.button 在构造函数中,并实现 applySyntax().

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class EmbedTool extends MarkdownEditor.Tool {
    constructor(editor) {
        super(editor, 'Embed video');        // tooltip, and .embed-video-btn
        this.shortcut = 'Ctrl+Shift+E';      // optional
        this.button = this.createButton('<svg>…</svg>');
    }

    applySyntax(event) {
        const dialog = MarkdownEditor.modal(event, 'fj:max-w-sm', `
            <input type="url" class="embed-url fj:me-input fj:w-full">
            <button type="button" class="embed-go fj:me-btn fj:me-btn-sm">Insert</button>
        `, 'Embed');

        dialog.querySelector('.embed-go').addEventListener('click', () => {
            const url = dialog.querySelector('.embed-url').value.trim();
            if (url) this.editor.insertText(`\n<iframe src="${url}"></iframe>\n`);
            dialog.close();
        });
    }

    destroy() {
        // Called by editor.destroy(). Release anything you registered here.
    }
}

new MarkdownEditor('#markdown-editor', { toolbar: ['bold', EmbedTool, 'preview'] });

通过配置 { tool: EmbedTool, config: {...} },它作为构造函数的第二个参数到达。环境 this.button 到 null 不渲染任何内容,这对于仅适用于某些配置的工具很有用。

需要注意的一件事: Tool的构造函数调用 createButton(),所以你的覆盖需要的任何东西都必须来自 this.editor,不是来自您之后分配的字段 super().

对话框助手

MarkdownEditor.modal 是内置链接和图像工具使用的对话框,因此您收集输入的工具看起来就像编辑器的其余部分,而不是发明自己的弹出窗口。它返回 <dialog> 元素,已打开,您可以查询字段并用其关闭 dialog.close().

javascript
1
2
// (event, size classes, body HTML, aria label)
const dialog = MarkdownEditor.modal(event, 'fj:max-w-sm', '…', 'Embed');

通过 event 你的 applySyntax() 已收到。帮助器读取它以查找单击来自哪个编辑器,因此该对话框将附加到该编辑器的包装器内并选择其主题。它会替换该编辑器中已打开的所有对话框,并在关闭时将其自身从 DOM 中删除,因此无需清理任何内容。

自己渲染 Markdown

如果您的工具嵌入了渲染内容(例如工具提示主体), editor.renderMarkdown() 生成与预览完全相同的 HTML,尊重您的 renderer 和 sanitizer.

javascript
const html = editor.renderMarkdown('**bold** text');

您自己的文本区域的工具栏

当对话框收集富文本时,为其提供真正的格式化工具,而不是重建它们。工具栏绑定到您传递的文本区域,您的 labels 都是继承的,并且没有添加预览按钮。

javascript
1
2
3
const body = dialog.querySelector('textarea');
const bar = editor.createToolbar(body, ['bold', 'italic', 'strikethrough']);
dialog.querySelector('.body-toolbar').appendChild(bar);

这不会管理工具栏的生命周期。当对话框被删除时,这些按钮会随着对话框一起清理,但是如果其中的工具注册了页面范围内的任何内容、文档侦听器或计时器,请在对话框关闭时自行释放它们。

翻译

自定义工具可以像内置工具一样进行翻译,由您赋予它们的标题键入。

javascript
labels: { 'Callout': 'Aviso', 'Embed video': 'Insertar video' }

CSS 类不受影响,所以 .callout-btn 停留 .callout-btn 每种语言。

📊 页脚(状态栏)

页脚位于编辑器下方,显示光标的行、列、文档的字符数以及可选的字数,所有这些都是实时更新的。默认情况下它是可见的,并且每个统计数据都可以独立切换。

钥匙类型默认描述
linebooleantrue显示当前行号。
colbooleantrue显示当前的列号。
charsbooleantrue显示总字符数。
wordsbooleanfalse显示总字数。默认关闭:设置为 true 启用。

使用示例

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Default — line, col, and chars visible
new MarkdownEditor('#editor');

// Disable the footer entirely
new MarkdownEditor('#editor', { footer: false });

// Hide only character count
new MarkdownEditor('#editor', { footer: { chars: false } });

// Hide line and column, keep character count
new MarkdownEditor('#editor', { footer: { line: false, col: false } });

// Show only line number
new MarkdownEditor('#editor', { footer: { col: false, chars: false } });

// Enable word count alongside the defaults
new MarkdownEditor('#editor', { footer: { words: true } });

// Show word count only
new MarkdownEditor('#editor', { footer: { line: false, col: false, chars: false, words: true } });

🔀 编辑模式

MarkdownEditor 提供两种不同的方式来编写和格式化内容。您可以在传统的以语法为中心的视图或现代的视觉优先体验之间切换。

  • plain (默认):一个干净、高性能的 Markdown 环境,其中语法(例如 **bold** 或者 # heading) 可见。开发人员和 Markdown 纯粹主义者的理想选择
  • hybrid:一种所见即所得的体验,可在您键入时实时呈现格式(粗体、斜体、标题),同时仍保持底层 Markdown 结构。
javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Default initialization (Plain Mode)
new MarkdownEditor('#markdown-editor');

// Explicit Plain Mode
new MarkdownEditor('#markdown-editor', {
    mode: 'plain'
});

// Hybrid (Visual) Mode
new MarkdownEditor('#markdown-editor', {
    mode: 'hybrid'
});

混合和普通模式预览:

混合模式

当您键入时,视觉格式会实时呈现。

普通模式(默认)

专注于原始 Markdown 语法以提供轻量级体验。

🌙 主题化

MarkdownEditor 自动从周围页面继承其主题:无需配置。编者读到 data-theme 从初始化时最近的祖先开始,因此它与您网站的开箱即用主题保持同步。

主题如何解决(优先顺序)

  1. theme option:在选项对象中传递的显式覆盖
  2. data-theme 于 <textarea>:直接在元素上设置
  3. data-theme 任何祖先:例如 <html>, <body>,或包装器 <div>

可用主题

'light' (默认), 'dark', 'snowberry', 'darkberry'

选项1,继承自 <html> 或任何祖先(零配置)
html
1
2
3
4
5
6
<html data-theme="dark">
  ...
  <textarea id="markdown-editor"></textarea>
  <script>
    new MarkdownEditor('#markdown-editor'); // picks up dark automatically
  </script>
选项 2:设置 data-theme 直接在 <textarea>
html
1
2
3
4
<textarea id="markdown-editor" data-theme="dark"></textarea>
<script>
  new MarkdownEditor('#markdown-editor');
</script>
选项 3:显式 theme 选项(覆盖所有内容)
javascript
1
2
3
new MarkdownEditor('#markdown-editor', {
    theme: 'dark'
});

🎨 通过 CSS 变量自定义主题

您可以通过覆盖其 CSS 变量来完全自定义编辑器的外观 .markdown-editor-wrapper 元素或任何 [data-theme] 选择器。所有颜色均使用 OKLCH 色彩空间 以获得感知上统一的结果。

多变的目的浅色默认深色默认
--color-base编辑背景oklch(100% 0 0)oklch(10.9% 0 0)
--color-on-base主要文本颜色oklch(22% 0 0)oklch(98% 0 0)
--color-primary主要重音(工具栏活动,链接)oklch(51.1% .262 277)oklch(66.4% .184 286)
--color-on-primary原色表面上的文字oklch(96.2% .018 272)oklch(10% .01 270)
--color-secondary次要口音oklch(59.1% .293 323)oklch(65% .18 220)
--color-accent突出显示重音符号(内联代码,斜体)oklch(54.1% .281 293)oklch(75% .18 50)
--color-neutral中性表面(边界、分隔线)oklch(15% 0 0)oklch(85% 0 0)
--color-error错误状态颜色oklch(57.7% .245 27)oklch(60% .22 30)
--border-radius编辑器框架的圆角0.25rem
自定义主题示例

覆盖任何变量 .markdown-editor-wrapper 编辑器初始化后,或定义自定义 [data-theme] 样式表中的块:

css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* Override individual variables */
.markdown-editor-wrapper {
    --color-primary: oklch(60% 0.2 30);   /* orange accent */
    --border-radius: 0.5rem;
}

/* Or define a full custom theme */
[data-theme="brand"] .markdown-editor-wrapper,
.markdown-editor-wrapper[data-theme="brand"] {
    --color-base:       oklch(15% 0.01 250);
    --color-on-base:    oklch(95% 0 0);
    --color-primary:    oklch(65% 0.22 145);   /* green */
    --color-on-primary: oklch(10% 0 0);
    --color-accent:     oklch(75% 0.18 60);
    --color-neutral:    oklch(80% 0 0);
    --border-radius:    0.75rem;
}
javascript
new MarkdownEditor('#markdown-editor', { theme: 'brand' });

🌐 界面语言

编辑器自己的文本、工具提示、菜单项、模式字段和按钮默认为英文。通过一个 labels 对象翻译它。键是英文字符串,即 gettext 的工作方式,因此您只需列出要更改的内容,而缺少的任何内容都会恢复为英文。

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
new MarkdownEditor('#markdown-editor', {
    labels: {
        'Bold': 'Gras',
        'Italic': 'Italique',
        'Heading': 'Titre',
        'Link': 'Lien',
        'Image link': 'Lien de l\'image',
        'URL': 'URL',
        'Alt text': 'Texte alternatif',
        'Apply': 'Appliquer',
        'Uploading...': 'Téléversement...',
    },
});

这与从右到左的内容一起最重要。阿拉伯语、乌尔都语和波斯语本身布局正确,但在从右到左的文本上仍然显示“粗体”的工具栏仅翻译了一半。

🏷 变量

添加用于插入占位符的工具栏下拉列表。当编写文档的人不是定义占位符语法的开发人员时很有用,例如电子邮件、发票或合同模板:他们选择一个可读的名称,并为其插入正确的语法。

javascript
1
2
3
4
5
6
7
8
9
new MarkdownEditor('#markdown-editor', {
    toolbar: ['bold', 'italic', 'link',
        { variables: [
            { label: 'Customer Name', value: '{{customer.name}}' },
            { label: 'Invoice No',    value: '{{invoice.number}}' }
        ]},
        'preview'
    ]
});

该工具是内联配置的 toolbar 数组,因此它在其他按钮中的位置取决于您。该按钮列出了标签,单击其中一个即可插入其标签 value 在光标处,替换任何选择。如果列表为空,则不会呈现任何内容。

预览中的示例值

给定一个条目 sample 在预览中显示该示例,而文本区域保留真实的占位符。没有其中之一的条目将按书面形式显示,因此您可以将两者混合使用。

javascript
1
2
3
4
5
6
7
{ variables: [
    { label: 'Customer Name', value: '{{customer.name}}', sample: 'Hannes' },
    { label: 'Invoice No',    value: '{{invoice.number}}' }
]}

// textarea : Hi {{customer.name}}, invoice {{invoice.number}}
// preview  : Hi Hannes, invoice {{invoice.number}}

分组

给出一个条目 items 而不是 value 渲染一个有标题的部分。分组条目和平面条目可以混合在一个列表中。

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{ variables: [
    { label: 'Today', value: '{{today}}', sample: '10 September 2026' },
    { label: 'Customer', items: [
        { label: 'Name',  value: '{{customer.name}}',  sample: 'Hannes' },
        { label: 'Email', value: '{{customer.email}}', sample: 'hannes@example.com' }
    ]},
    { label: 'Invoice', items: [
        { label: 'Number', value: '{{invoice.number}}' }
    ]}
]}

要知道的事情

  • 编辑器从不解析变量。 它插入文本,然后您的应用程序通常在服务器端替换实际值。 sample 仅影响预览显示的内容。
  • 标签显示为纯文本,因此标签中的标记永远不会呈现。
  • 格式错误的条目将被跳过而不是抛出,并且当未配置任何可用的内容时,根本不会呈现按钮。

🖋 自定义渲染器

预览呈现为 标记的 并用消毒 DOM纯化。两者都可以更换。当您的应用程序已经使用另一个库呈现 Markdown 并且您希望预览与生产完全匹配时,请使用此选项。

这两个选项都是普通函数,它们接受一个字符串并返回一个字符串,因此任何解析器和任何清理程序都可以工作。

javascript
1
2
3
4
5
6
7
8
9
import MarkdownIt from 'markdown-it';
import taskLists from 'markdown-it-task-lists';
import MarkdownEditor from 'markdown-text-editor';

const md = new MarkdownIt({ linkify: true, breaks: true }).use(taskLists);

new MarkdownEditor('#markdown-editor', {
    renderer: markdown => md.render(markdown)
});

DOMPurify 仍然在输出上运行,因此您无需配置任何内容即可保持 XSS 保护。

定制消毒液

仅当默认删除渲染器发出的内容时才需要。 DOMPurify 删除 <iframe> 默认情况下,因此视频嵌入需要明确允许。 DOMPurify 必须在您自己的代码中导入,因为与编辑器捆绑的副本是内部的。

javascript
1
2
3
4
5
6
7
8
9
import DOMPurify from 'dompurify';

new MarkdownEditor('#markdown-editor', {
    renderer:  markdown => md.render(markdown),
    sanitizer: html => DOMPurify.sanitize(html, {
        ADD_TAGS: ['iframe'],
        ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder']
    })
});

使用脚本标签而不是捆绑器,将 DOMPurify 与编辑器一起加载:

html
1
2
<script src="https://cdn.jsdelivr.net/npm/dompurify"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-text-editor"></script>

要知道的事情

  • 仅限预览。 混合模式的实时格式使用单独的内部渲染器,并且不受影响。
  • **任务列表需要插件支持。**可通过查找找到可点击的复选框 input[type="checkbox"] 在输出中,所以 markdown-it 需要 markdown-it-task-lists。如果编辑器检测到任务列表语法且没有复选框,则会记录一条警告。
  • 两者必须同步并返回一个字符串。一个 async 函数写入 [object Promise] 进入预览。
  • 更换消毒剂就取代了您的保护。 传递,例如 html => html 完全禁用清理,并且仅对完全信任的内容是安全的。

🎨 设置内部元素的样式

CSS 变量涵盖了大多数主题,因为它们是继承的,因此设置一个 .markdown-editor-wrapper 到达工具栏、按钮、预览和页脚。仅当没有变量公开您需要的内容时才使用类名。

css
1
2
3
4
5
/* Variables set on the wrapper inherit down to every child */
.markdown-editor-wrapper {
    --border-radius: 12px;   /* rounds the editor and its toolbar buttons */
    --color-primary: oklch(60% 0.2 30);
}

类参考

这些类名对于样式来说是稳定且安全的。

班级元素
.markdown-editor-wrapper包裹整个编辑器的外容器
.toolbar编辑区域上方的工具栏条
.markdown-btn单独的工具栏按钮
.preview-btn预览/全屏切换按钮
.editor-layout网格并排容纳编辑区域和预览
.textarea-wrapper编辑区域周围的包装
.editor-textarea底层文本区域元素
.display-layer渲染格式化图层,仅限混合模式
.preview-wrapper预览专栏
.preview-content在预览中渲染的 Markdown
.editor-footer编辑器下方的状态栏
.find-replace-panel查找并替换面板
css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* Some variables are set by the component on itself, which beats an
   inherited value. Target the element directly for those. */
.markdown-editor-wrapper .markdown-btn {
    --btn--font-size: 0.875rem;
}

/* And use classes for anything no variable exposes */
.markdown-editor-wrapper .toolbar {
    border-bottom: 2px solid oklch(60% 0.2 30);
}

定位一位编辑者

在一页上有多个编辑器时,将其中一个编辑器定位为 data-editor。包装器反映了它的 id <textarea>, 所以 <textarea id="notes"> 给你 [data-editor="notes"]。 id 本身保留在文本区域中,所以 getElementById 继续工作。

css
1
2
3
4
/* one editor only */
[data-editor="notes"] {
    --border-radius: 0;
}

🪟 全屏和分层(z-index)

在全屏模式下,编辑器使用 z-index: 10000,这会清除大多数 UI 框架为固定标题和覆盖层保留的层。如果固定标题或侧边栏仍然覆盖编辑器,请提高此值。

覆盖它 --mte-fullscreen-z-index。该值仅适用于全屏,并且会继承,因此在任何祖先上设置它都会覆盖其下面的每个编辑器。

css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* every editor on the page */
.markdown-editor-wrapper {
    --mte-fullscreen-z-index: 20000;
}

/* one section only - the value inherits down */
#admin-panel {
    --mte-fullscreen-z-index: 20000;
}

/* a single editor, by its textarea id */
[data-editor="notes"] {
    --mte-fullscreen-z-index: 20000;
}

内容API

MarkdownEditor 的核心优势之一是它保留了底层 <textarea> 完美同步。无论您使用现代 JavaScript 框架还是传统后端(例如 Django、PHP 或 Laravel),工作流程都保持简单且原生。

阅读和写作内容

1.本土方式(推荐)

由于编辑器增强了标准文本区域,因此您可以使用熟悉的 DOM 方法。这是无需学习新 API 即可与数据交互的最快方式。

javascript
1
2
3
4
5
// Retrieve content via ID
const markdown = document.getElementById('markdown-editor').value;

// Set content via ID (The editor UI updates automatically)
document.getElementById('markdown-editor').value = "# New Heading Content";

2.使用变量引用

如果您有对 textarea 元素的引用,则可以直接使用它:不需要特定于库的 API。

javascript
1
2
3
4
5
6
7
const textarea = document.getElementById('markdown-editor');

// Retrieve content
const markdown = textarea.value;

// Set content (the editor UI reflects this immediately)
textarea.value = "## Updated via JS";

3.设置初始内容服务器端

设置初始内容的推荐方法是直接在 <textarea> HTML:这可以自然地与每个后端框架(Django、Laravel、Rails、PHP 等)配合使用,并且编辑器会在 init 上自动呈现它。

html
1
2
<!-- Recommended: set content server-side -->
<textarea id="markdown-editor"># Hello World</textarea>

要在运行时读取或更新内容,请使用本机 textarea 价值。称呼 editor.render() 更新后刷新预览和混合层。

javascript
1
2
3
4
5
6
7
8
const textarea = document.getElementById('markdown-editor');

// Read
const markdown = textarea.value;

// Update at runtime
textarea.value = '# New content';
editor.render();

4.拆掉编辑器: destroy()

称呼 editor.destroy() 删除编辑器 DOM 包装并恢复原始状态 <textarea> 到其在文档中的位置。在单页应用程序中卸载视图时很有用。

javascript
1
2
3
4
const editor = new MarkdownEditor('#markdown-editor');

// Remove the editor and restore the plain textarea
editor.destroy();

对变化做出反应 onChange

通过一个 onChange 每次内容更改时都会收到回调通知。接收当前的 Markdown 字符串。

javascript
1
2
3
4
5
const editor = new MarkdownEditor('#markdown-editor', {
    onChange(value) {
        console.log('Content changed:', value.length, 'characters');
    }
});

草稿自动保存 localStorage

使用 onChange 每次击键时保存草稿。在初始化编辑器之前通过预填充文本区域来恢复它。

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const DRAFT_KEY = 'my-page-draft';

// Restore saved draft before init (only if textarea starts empty)
const textarea = document.getElementById('markdown-editor');
const saved = localStorage.getItem(DRAFT_KEY);
if (saved && !textarea.value) textarea.value = saved;

// Save on every change
const editor = new MarkdownEditor('#markdown-editor', {
    onChange(value) {
        localStorage.setItem(DRAFT_KEY, value);
    }
});

// Clear draft after successful form submission
document.querySelector('form').addEventListener('submit', () => {
    localStorage.removeItem(DRAFT_KEY);
});

表格提交

因为 MarkdownEditor 是直接构建在原生之上的 <textarea>,它开箱即用,与每个后端框架(Django、Laravel、PHP、Ruby on Rails 等)兼容。

这就是“本地优先”理念的闪光点。您无需在提交表单之前手动同步数据。浏览器将编辑器视为标准输入字段。

html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<form method="POST" action="/api/submit">
  <textarea id="markdown-editor" name="content" class="h-48" rows="5">
    # Initial Content
  </textarea>

  <button type="submit">Submit to Server</button>
</form>

<script>
  // Just initialize it. That's it.
  new MarkdownEditor('#markdown-editor');
</script>

笔记: MarkdownEditor 插件必须初始化

只需使用标准 HTML <form>。这 name 文本区域上的属性是您的服务器将用来识别内容的属性。

🚀 为什么它会改变后端的游戏规则

由于编辑器保留了原生 <textarea> 行为,您的服务器将数据作为标准字符串处理。需要零额外逻辑:没有 preventDefault() 并且没有说明书 FormData 建造。

💡 为什么这是一个“杀手级功能”:

大多数编辑器(如 Quill、Editor.js、simpleMDE、easyMDE)以复杂的 JSON 结构保存数据。如果开发人员使用这些,他们必须重写他们的数据库模式和渲染逻辑。

使用 MarkdownEditor,开发人员可以使用旧网站,替换普通网站 <textarea> 与您的编辑器一起使用,后端甚至不知道它发生了变化。它只是收到与往常相同的原始文本,但用户获得了 10 倍更好的体验。

框架/语言如何访问 Markdown 内容
PHP$_POST['content']
姜戈request.POST.get('content')
Node.js(Express)req.body.content
拉维尔$request->input('content')
红宝石 on Railsparams[:content]

🖼️ 高级图像上传

本地处理图像上传:而​​不是依赖缓慢、占用大量内存的 Base64 字符串,这对于性能和 SEO 来说都是一个重大胜利。

粘贴和放置

将屏幕截图粘贴到编辑器中或将图像文件拖到编辑器中,然后通过与工具栏按钮相同的端点上传。占位符会立即出现,因此编辑器不会看起来冻结,并且一旦上传落地,它就会被替换为真实路径。如果上传失败,占位符将替换为可见标记,而不是默默消失。

这需要 uploadUrl 配置如下。如果没有它,粘贴图像不会执行任何操作,并且编辑器会记录一条警告,解释要添加的内容,因为替代方案将是数据库中的 Base64 字符串。

一次上传多个文件,因此图像按照到达的顺序排列。

配置选项

图像工具支持 fileInput 配置来处理直接服务器上传。

  • accept:定义允许的图像格式数组(例如“webp”、“avif”)
  • uploadUrl:指定后端端点 File 对象将通过发送 POST
  • params:可选对象,用于与图像文件一起发送附加数据(例如 CSRF 令牌、用户 ID 或文件夹名称)

使用示例(完整配置)

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const options = {
  placeholder: 'Start writing...',
  toolbar: [
    'link',
    {
      image: {
        fileInput: {
            accept: ['webp', 'avif'], // restrict the image upload format
            uploadUrl: '/api/upload', // Your upload endpoint
            params: {
                _token: 'your_csrf_token_here', // Essential for Laravel/Django
                folder: 'blog_posts'
            }
        },
        // Supports boolean: true/false OR object: { required: true }
        altInput: { required: true }
      }
    },
    'preview'
  ],
}
const editor = new MarkdownEditor('#markdown-editor', options);

📡 服务器集成

请求

编辑发来一条 POST 请求为 multipart/form-data。默认情况下,它包括:

  • image_file: 实际的文件对象
  • image_alt:用户输入的替代文本
  • ...加上中定义的任何自定义数据 params 目的

所需的回应

要确认上传成功并将图像插入编辑器,您的服务器必须返回以下 JSON 结构:

json
1
2
3
4
{
  "success": true,
  "image_path": "https://cdn.yourdomain.com/uploads/image.webp"
}

注意:确保您使用密钥 image_path 为上传图像的 URL。

您也可以返回 image_alt,仅当用户将 alt 字段留空时才用作建议。他们自己输入的任何内容总是会获胜,因此从文件名或图像识别中派生替代文本的服务器可以在不覆盖真实描述的情况下提供替代文本。

json
1
2
3
4
5
{
  "success": true,
  "image_path": "https://cdn.yourdomain.com/uploads/image.webp",
  "image_alt": "Two people looking at a laptop"
}

失败时返回 success: false 与一个 error 或者 message 细绳。该文本会呈现给用户,因此请使其成为他们可以采取行动的内容。

图像替代文本验证(altInput)

为了确保您的内容保持可访问性和 SEO 友好性,MarkdownEditor 默认强制执行替代文本验证。

  • 默认行为:如果 altInput 没有定义,默认为 { required: true }
  • 强制可访问性:在提供替代说明之前,将阻止用户插入图像

1.默认(无需配置)

javascript
1
2
3
4
// Alt text is REQUIRED by default
image: {
  fileInput: { uploadUrl: '/api/upload' }
}

2.速记(禁用验证)

如果您想允许没有描述的图像,只需将布尔值设置为 false.

javascript
1
2
3
image: {
  altInput: false // Users can now skip the alt text field
}

3.基于对象(显式)

javascript
1
2
3
4
5
image: {
    altInput: {
        required: false // Disables alt text validation — users can skip the alt field
    }
}

标准图像使用(否 fileInput)

如果 fileInput 未配置时,编辑器默认为简单的基于 URL 的模式。如果您的用户主要链接到外部图像主机,那么这是理想的选择。

javascript
1
2
3
4
5
6
7
8
const options = {
  toolbar: [
    'link',
    'image',
    'preview'
  ],
}
const editor = new MarkdownEditor('#markdown-editor', options);

💡 为什么使用参数?

在 Laravel 或 Django 等框架中,如果没有 CSRF 令牌,则无法上传文件。通过添加 _token 到 params 对象,您的请求将无缝地通过后端的安全中间件,维护服务器端控制器的“零逻辑”理念。

⌨️ 键盘快捷键

常见的格式化操作可以直接从键盘触发,无需触摸工具栏。每个快捷方式也显示在相应的工具栏按钮的工具提示中。

捷径行动
Ctrl + B  /  ⌘ B切换 粗体
Ctrl + I  /  ⌘ I切换斜体
Ctrl + K  /  ⌘ K插入链接
Ctrl + `  /  ⌘ `切换内联 Code
Ctrl + Shift + S  /  ⌘ ⇧ S切换删除线
Ctrl + 1  /  ⌘ 1标题 1
Ctrl + 2  /  ⌘ 2标题 2
Ctrl + 3  /  ⌘ 3标题 3
Ctrl + L  /  ⌘ L切换项目符号列表
Ctrl + Z  /  ⌘ Z撤消
Ctrl + Shift + Z  /  ⌘ ⇧ Z重做
Tab缩进选定的行
Shift + Tab减少缩进选定的行
Ctrl + F  /  ⌘ F打开查找面板
Ctrl + H  /  ⌘ H打开查找和替换面板
Ctrl + Shift + F  /  ⌘ ⇧ F切换全屏预览
F11切换全屏预览
Escape关闭查找面板/退出全屏预览

🔍 查找和替换

编辑器内提供内置查找和替换面板:无需浏览器扩展或单独的工具。

  • 按 Ctrl + F (或者 ⌘ F) 打开 查找 面板
  • 按 Ctrl + H (或者 ⌘ H) 打开 查找和替换 面板
  • 搜索不区分大小写并显示实时比赛计数器(例如 3 of 12)
  • 使用 ▲ / ▼ 按钮导航匹配或 Enter / Shift + Enter
  • 替换 替换当前突出显示的匹配项; 全部替换 一次替换所有出现的内容
  • 按 Escape 关闭面板并将焦点返回到编辑器

该面板浮动在编辑器内容区域的右上角,不会中断写入。

🔗 来自粘贴 URL 的链接

选择一些文本,在其上粘贴 URL,然后您会得到一个 Markdown 链接,而不是替换您选择的内容的 URL。

markdown
1
2
Read the docs                        <- select "the docs", paste https://example.com
Read [the docs](https://example.com) <- what you get

规则刻意严格:只有裸露的 http 或者 https 不带空格的 URL 将以这种方式处理。粘贴恰好包含 URL 的句子或在未选择任何内容的情况下进行粘贴,其行为与普通粘贴类似。

特征

🔌 原生表单集成

工作原理与标准完全相同 <textarea>。没有复杂的 API:只需使用 value 或者 name 属性。它“只适用于”PHP、Django 或 Node.js 中的标准 HTML 表单提交。

🖼️ 高级图像上传

通过 API 配置本机服务器上传。通过在您自己的 CDN 上托管图像,避免使用繁重的 Base64 字符串,以确保更快的页面加载和卓越的 SEO。

🔀 混合模式和普通模式

之间切换 混合(所见即所得) 视觉编辑经验或 普通降价 传统编码感觉的模式。

🚀 高性能

一个 gzip 压缩后大小为 53 KB 捆绑包(缩小为 252 KB,包含 CSS)针对“重内容”进行了优化。处理大量文档和大文件,不会出现任何输入延迟或性能下降。去抖预览更新、缓存样式计算和无冲突键盘处理:因此 Tab 和 Enter 始终只做一件事。

🌍 内置 RTL 支持

对阿拉伯语、乌尔都语和波斯语等从右到左语言的本机支持。非常适合构建全球可访问的应用程序。

✨ 语法高亮

通过清晰的代码和 Markdown 格式增强可读性。

🌙 自适应主题

包括自动深色模式支持。它与您的系统设置或 水果果酱用户界面 图书馆提供无缝的视觉体验。

📝 智能编辑

GitHub 风格的自动列表延续,适用于有序列表、无序列表和清单:按 Enter 编辑继续这个模式。预览窗格中的复选框可单击并立即同步回 Markdown 源。

📱 完全响应

流畅、移动优先的用户界面,完美适应台式机、平板电脑和智能手机,以便随时随地进行编辑。

📦 普遍支持

兼容于 ESM、UMD、CommonJS 和 IIFE。通过 CDN 开箱即用(<script src>)、npm 或任何捆绑器(Vite、webpack、Rollup):无需额外配置。

♿ 默认可访问

内置完整的 ARIA 支持:工具栏地标、带标签的预览区域、屏幕阅读器友好的按钮、 aria-pressed 在预览开关上, disabled 和 aria-disabled 在非活动工具上,并在模式关闭时正确恢复焦点。

🛡️ 零 CSS 冲突

编辑器样式完全适用于 .markdown-editor-wrapper。 Tailwind 的全局预检被排除,因此编辑器可以安全地与 Bootstrap、Tailwind 或任何其他框架一起使用,而不会破坏它们的风格。

⌨️ 键盘快捷键

Ctrl+B, Ctrl+I, Ctrl+K, Ctrl+`, Ctrl+Shift+S:无需触摸鼠标的常见格式化操作。每个快捷方式都显示在工具栏按钮的工具提示中。

🔍 查找和替换

按 Ctrl+F 寻找或 Ctrl+H 打开查找和替换。不区分大小写的搜索,具有实时匹配计数器、下一个/上一个导航、单个替换和全部替换:无需离开编辑器。

🔒 XSS 安全预览

渲染的预览通过以下方式进行清理 DOM纯化 在写入 DOM 之前。精心设计的 Markdown 输入中的脚本标签、内联事件处理程序和恶意 URL 都会被自动删除:无需配置。

▶️实时预览

在您键入时查看立即呈现的 Markdown。

🔗 轻松集成

只需最少的设置即可无缝集成到任何 Web 项目中。

🛠️ 可定制的工具栏

动态配置和重新排序工具栏选项,例如粗体、斜体等。

完整配置示例

使用这个综合示例来初始化 MarkdownEditor 的所有主要功能,包括自定义工具栏排序和高级图像上传处理。

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const editor = new MarkdownEditor('#markdown-editor', {
    mode: 'hybrid',
    placeholder: 'Start writing...',
    footer: {
        line: true,
        col: true,
        chars: true,
        words: true,
    },
    onChange(value) {
        console.log('Content updated:', value.length, 'characters');
    },
    labels: {
        'Bold': 'Gras',
        'Italic': 'Italique',
        'Uploading...': 'Téléversement...',
    },
    toolbar: [
        'heading', 'bold', 'italic', 'strikethrough', 'blockquote',
        'ul', 'ol', 'checklist',
        'code', 'codeblock', 'hr', 'table',
        {
            image: {
                fileInput: {
                    accept: ['webp', 'avif', 'png'],
                    uploadUrl: '/api/upload'
                }
            }
        },
        'link', 'undo', 'redo', 'indent', 'outdent', 'preview'
    ],
});

// Read content natively
const markdown = document.getElementById('markdown-editor').value;

// destroy() when the view unmounts (SPAs)
// editor.destroy();

觉得这有用吗?

GitHub 之星可以帮助其他开发人员发现该编辑器。它是的一部分 弗鲁贾姆。那里的明星也有帮助。

在 GitHub 上加星标