Markdown MD 编辑器

基本用法

v-model双向绑定输入容器内的值;当内容发生变化时,出发content-change事件,返回当前内容。
<template>
  <input v-model="content" />
  <d-editor-md v-model="content" :md-rules="mdRules" base-url="https://test-base-url" @content-change="valueChange" @preview-content-change="previewChange"></d-editor-md>
</template>

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref('# This is the title');
    const mdRules = reactive({
      linkify: {
        fuzzyLink: false,
      },
    });

    const valueChange = (val) => {
      console.log(val);
    };

    const previewChange = (val) => {
      console.log(val);
    };

    return { content, mdRules, valueChange, previewChange };
  },
});
</script>

自定义渲染

自定义从 md 到 html 的渲染规则,也可自定义XSS过滤规则,放开指定标签。
<template>
  <d-editor-md v-model="content" :custom-renderer-rules="customRendererRules" :custom-xss-rules="customRenderRules"></d-editor-md>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref('[link](#test)\n<kbd>kbd 标签渲染</kbd>');
    const customRenderRules = ref([
      {
        key: 'kbd',
        value: [], // 为空表示过滤所有属性,放开属性则添加对应项,如['id', 'style']
      },
      {
        key: 'input',
        value: null, // value值为null,则对应标签不会被渲染
      }
    ])
    const customRendererRules = ref([
      {
        key: 'link_open',
        value: function (tokens, idx, opts, env, self) {
          const defaultRender = function (nTokens, index, options, ev, that) {
            return that.renderToken(nTokens, index, options);
          };
          const token = tokens[idx];
          const attrs = token.attrs;
          const hrefIndex = token.attrIndex('href');
          const href = attrs[hrefIndex][1];
          if (typeof window !== 'undefined') {
            attrs[hrefIndex][1] = location.href + href;
          }
          token.attrPush(['target', '_blank']);
          token.attrPush(['id', href]);
          return defaultRender(tokens, idx, opts, env, self);
        },
      },
    ]);
    return { content, customRendererRules, customRenderRules };
  },
});
</script>

自定义工具栏

+
自定义编辑器的工具栏
<template>
  <d-editor-md v-model="content" :toolbar-config="toolbarConfig" :custom-toolbars="customToolbars"></d-editor-md>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref('');
    const toolbarConfig = ['add',
      ['undo', 'redo'],
      ['h1', 'h2', 'bold', 'italic', 'strike', 'underline', 'color', 'font'],
      ['ul', 'ol', 'checklist', 'code', 'link', 'image', 'table'],
      'fullscreen',
    ];
    const customToolbars = {
      add: {
        id: 'add',
        name: '新增',
        exitName: '新增',
        type: 'button',
        icon: `<span>+</span>`,
        shortKey: 'ALT+K',
        handler: () => {
          console.log('自定义工具点击事件');
        },
      },
      undo: {
        id: 'undo',
        name: '撤销',
        exitName: '撤销',
        type: 'button',
        icon: `<svg width="16px" height="14px" viewBox="0 0 16 14">
        <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
          <g fill="#293040">
            <path d="M11,5 C13.7614237,5 16,7.23857625 16,10 C16,12.7614237 13.7614237,15 11,15 L7,15 L7,14 L11,14 C13.209139,14 15,12.209139 15,10 C15,7.790861 13.209139,6 11,6 L5,6 L5,10 L0,5.5 L5,1 L5,5 L11,5 Z" id="路径"></path>
          </g>
        </g>
        </svg>`,
        shortKey: 'Ctrl+M',
        handler: () => {
          console.log('覆盖原有工具栏功能事件');
        },
      }
    };
    return { content, toolbarConfig, customToolbars };
  },
});
</script>

纯渲染模式

使用 MDRender 进行单独渲染
<template>
  <d-md-render :content="content" :md-rules="mdRules" base-url="https://test-base-url"></d-md-render>
</template>

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref('# h1 \n [link](#test)');
    const mdRules = reactive({
      linkify: {
        fuzzyLink: false,
      },
    });

    return { content, mdRules };
  },
});
</script>

单列渲染模式

通过 mode 控制不同的显示模式
<template>
  <d-button variant="solid" color="primary" @click="handleModeChange" style="margin-bottom:8px">切换编辑/只读模式</d-button>
  <d-editor-md
    v-model="content"
    :mode="mode"
    :md-rules="mdRules"
    base-url="https://test-base-url"
    @content-change="valueChange"
  ></d-editor-md>
</template>

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref('# This is the title');
    const mdRules = reactive({
      linkify: {
        fuzzyLink: false,
      },
    });

    const valueChange = (val) => {
      console.log(val);
    };

    const mode = ref('editonly');

    const handleModeChange = () => {
      if (mode.value === 'editonly') {
        mode.value = 'readonly';
      } else {
        mode.value = 'editonly';
      }
    };

    return { content, mode, mdRules, valueChange, handleModeChange };
  },
});
</script>

配置图片文件上传

设置 imageUploadToServer 后,编辑器对粘贴操作也将进行监听,若有图片也将触发 imageUpload 事件。
<template>
  <d-editor-md v-model="content" :image-upload-to-server="true" @content-change="valueChange" @image-upload="imageUpload"></d-editor-md>
</template>

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref('# This is the title');

    const valueChange = (val) => {
      console.log(val);
    };

    const imageUpload = ({ file, callback }) => {
      let message;
      const rFilter = /^(image\/bmp|image\/gif|image\/jpge|image\/jpeg|image\/jpg|image\/png|image\/tiff)$/i;
      if (!rFilter.test(file.type)) {
        console.log(rFilter, file.type);
        message = 'Please choose bmp/jpg/jpge/png/gif/tiff type picture to upload';
      } else if (file.size / (1024 * 1024) > 1) {
        message = 'Please choose a picture smaller than 1M to upload';
      }

      if (message) {
        // throw the error message by yourself
        return false;
      } else {
        new Promise((resolve) => {
          const xhr = new XMLHttpRequest();
          xhr.open('POST', 'https://xxx.xxx.com/v1/xxx');
          xhr.setRequestHeader('yourKey', 'yourValue');

          xhr.addEventListener(
            'load',
            (evt) => {
              const result = JSON.parse(xhr.responseText);
              resolve(result);
            },
            false
          );

          const fd = new FormData();
          fd.append('file', file);
          xhr.send(fd);
        }).then((res) => {
          if (res.status === 'success') {
            callback({ name: file.name, imgUrl: res['imgUrl'], title: res['imgTitle'] });
          } else {
            // throw your error message
          }
        });
      }
    };

    return { content, valueChange, imageUpload };
  },
});
</script>

checkbox 渲染

通过配置md-plugins checkbox插件,进行checkbox渲染于checked变更响应。

<template>
  <d-editor-md
    v-model="content"
    :md-rules="mdRules"
    base-url="https://test-base-url"
    @content-change="valueChange"
    @checked-change="onCheckedEvent"
  />
  <!-- <d-editor-md
    v-model="content"
    :md-rules="mdRules"
    :md-plugins="plugins"
    base-url="https://test-base-url"
    @content-change="valueChange"
    @checked-change="onCheckedEvent"
  /> -->
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue';
// import { checkbox } from 'vue-devui/editor-md'; // demo无法进行import,使用时请放开注释

const content = ref('[x] checked \n[ ] unchecked // demo无法进行import,使用时请放开代码中注释');
const mdRules = reactive({
  linkify: {
    fuzzyLink: false,
  },
});

const valueChange = (val) => {
  console.log(val);
};

const plugins = reactive([
  {
    // plugin: checkbox,
    opts: {
      idPrefix: 'devui',
      disable: false,
    },
  },
]);

const onCheckedEvent = (val) => {
  console.log('demo', val);
  content.value = val;
};
</script>

数学公式 渲染

通过配置md-plugins katex插件,进行数学公式渲染。
<template>
  <d-editor-md
    v-model="content"
  >
  </d-editor-md>
  <!-- <d-editor-md
    v-model="content"
    :md-plugins="plugins"
  >
  </d-editor-md> -->
</template>

<script>
import { defineComponent, reactive, ref } from 'vue';
// import mk from '@iktakahiro/markdown-it-katex';  // 请首先安装 @iktakahiro/markdown-it-katex 依赖

export default defineComponent({
  setup() {
    const content = ref(`$E = mc^2$
$\\sqrt{3x-1}+(1+x)^2$  // DEMO无法进行import,使用时请放开代码中注释
    `);

    const mdPlugins = [{
      // plugin: mk
    }];

    return { content, mdPlugins };
  },
});
</script>

<style>
@import 'katex/dist/katex.min.css';  /* 请首先安装 katex 依赖 */
</style>

PlantUML 渲染

通过配置md-plugins plantuml插件,进行plantuml图渲染。
<template>
  <d-editor-md
    v-model="content"
  >
  </d-editor-md>
  <!-- <d-editor-md
    v-model="content"
    :md-plugins="plugins"
  >
  </d-editor-md> -->
</template>

<script>
import { defineComponent, reactive, ref } from 'vue';
// import PlantUml from 'markdown-it-plantuml';  // 请首先安装 markdown-it-plantuml 依赖

export default defineComponent({
  setup() {
    const content = ref(`// DEMO无法进行import,使用时请放开代码中注释
@startuml
Alice -> "Bob()" : Hello
"Bob()" -> "This is very long" as Long
' You can also declare:
' "Bob()" -> Long as "This is very long"
Long --> "Bob()" : ok
@enduml`);

    const mdPlugins = [{
      // plugin: PlantUml,
      // opts: {server: 'https://www/plantuml.com/plantuml'} // 自定义server可参考plantuml官方文档进行搭建
    }];

    return { content, mdPlugins };
  },
});
</script>

emoji渲染

通过配置md-plugins emoji插件,进行emoji表情渲染。具体使用方式参考示例代码。

<template>
  <d-editor-md v-model="content" :md-plugins="plugins"></d-editor-md>
</template>

<script>
import { defineComponent,ref } from 'vue';
// import { full as emoji } from 'markdown-it-emoji'; // 请首先安装 markdown-it-emoji 依赖

export default defineComponent({
  setup(){
    const content = ref(':joy:');
    const plugins = [];
    /* const plugins = [{
      plugin: emoji,
    }]; */

    return {content,plugins}
  }
})

</script>

配置快速提示

设置 hintConfig 后,可用于支持@选择用户等场景。
<template>
  <d-editor-md
    v-model="content"
    :placeholder="'You can enter @ associate member, enter # to associate an order number...'"
    :hint-config="hintConfig"
    :fullscreen-z-index="1000"
    @content-change="valueChange"
  >
    <template #hintTemplate>
      <ul class="list-menu" v-if="hintList && hintList.length">
        <li class="menu-item" v-for="(item, index) of hintList" @click="hintItemClick(item)">{{ `${item.itemText}`}}</li>
      </ul>
    </template>
  </d-editor-md>
</template>

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref('');

    const valueChange = (val) => {
      console.log(val);
    };

    const hintList = ref([]);

    const hintItemClick = (item) => {
      hintCallback.value && hintCallback.value(item.insertText || item.itemText);
    };

    const hintCallback = ref();

    const hintConfig = {
      '#': (e) => {
          const { callback, cursorHint, prefix } = e;
          const numberList = [
            {
              itemText: '00001',
              insertText: '[00001](#00001)'
            },
            {
              itemText: '00002',
              insertText: '[00002](#00002)'
            },
            {
              itemText: '00003',
              insertText: '[00003](#00003)'
            },
            {
              itemText: '00004',
              insertText: '[00004](#00004)'
            }
          ];
          hintList.value = numberList.filter((item) => item.itemText.indexOf(cursorHint) !== -1);
          hintCallback.value = callback;
      },
      '@': {
        handler: (e) => {
          const { callback, cursorHint, prefix } = e;
          const userList = [
            {
              itemText: 'User1'
            },
            {
              itemText: 'User2'
            },
            {
              itemText: 'User3'
            },
            {
              itemText: 'User4'
            }
          ];
          hintList.value = userList.filter((item) => item.itemText.indexOf(cursorHint) !== -1);
          hintCallback.value = callback;
        },
      },
      throttleTime: 200,
    };

    return { content, valueChange, hintConfig, hintList, hintCallback, hintItemClick };
  },
});
</script>
<style>
.list-menu {
  padding: 8px;
  margin: 0;
  width: 100px;
}
.menu-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 4px;
  cursor: pointer;
}
.menu-item:hover {
  background-color: var(--devui-list-item-hover-bg, #f2f5fc);
  color: var(--devui-list-item-hover-text, #526ecc);
}
</style>

TOC目录渲染

支持TOC目录生成
<template>
  <d-editor-md v-model="content" :base-url="baseUrl"></d-editor-md>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref(`[TOC]
# Directory

## Level-1 Directory
This is a Level-1 directory.

### Level-2 Directory
This is a Level-1 directory.

## Level-1 Directory
This is a Level-1 directory.
`);
    const baseUrl = location.href;

    return { content, baseUrl };
  },
});
</script>

mermaid 渲染

支持mermaid流程图、甘特图、时序图等图表渲染
<template>
  <d-editor-md v-model="content" :fullscreen-z-index="1000"></d-editor-md>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const content = ref(`# Flow Chart
\`\`\`mermaid
flowchart LR
A[Hard] -->|Text| B(Round)
B --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]
\`\`\`

# Gantt
\`\`\`mermaid
gantt
    section Section
    Completed :done,    des1, 2014-01-06,2014-01-08
    Active        :active,  des2, 2014-01-07, 3d
    Parallel 1   :         des3, after des1, 1d
    Parallel 2   :         des4, after des1, 1d
    Parallel 3   :         des5, after des3, 1d
    Parallel 4   :         des6, after des4, 1d
\`\`\`

# Class Diagram
\`\`\`mermaid
classDiagram
Class01 <|-- AveryLongClass : Cool
<<Interface>> Class01
Class09 --> C2 : Where am I?
Class09 --* C3
Class09 --|> Class07
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
class Class10 {
  <<service>>
  int id
  size()
}
\`\`\`

# State Diagram
\`\`\`mermaid
stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
\`\`\`

# Pie
\`\`\`mermaid
pie
"Dogs" : 386
"Cats" : 85
"Rats" : 15
\`\`\`

# Sequence Diagram
\`\`\`mermaid
sequenceDiagram
Alice->>John: Hello John, how are you?
loop Healthcheck
    John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
\`\`\`
`);

    return { content };
  },
});
</script>

EditorMd 参数

参数名类型默认值说明
v-modelstring''编辑器内容双向绑定
optionsobject{}编辑器初始化时,自定义配置,可参考CodeMirror Options
base-urlstring--设置渲染到 html 时,为相对 url 添加的 baseUrl
custom-parse(html: string) => string--自定义对渲染后的 html 处理,需要接收渲染后的 html,返回自定义处理后的 html
md-rulesobject{}设置 markdown 对字符串的处理方式, 可参考markdown-it
md-pluginsMdPlugin[]--设置 markdown-it 插件
hintConfigMdHintConfig[]--设置 快速提示 配置
mode'editonly' | 'readonly' | 'normal''normal'只写/只读/双栏显示模式选择,默认 'normal' 双栏模式显示
custom-renderer-rulesICustomRenderRule[][]自定义 markdown 对节点的渲染方式,每条规则需要指定对应节点 key,并自定义渲染函数
custom-xss-rulesICustomXssRule[][]自定义 xss 对某种 tag 的过滤方式,每条规则需要指定 tag, 并给出需要加入白名单的属性数组
placeholderstring''编辑器无内容是的提示信息
fullscreen-z-indexnumber10编辑器全屏状态的 z-index
image-upload-to-serverbooleanfalse是否打开图片自定义上传开关(打开后将将监听图片的复制,toolbar 图片功能上传,传出事件回调)
editor-container-heightnumber--可选,编辑器内容区高度
toolbar-configArray(string)[['undo', 'redo'],['h1', 'h2', 'bold', 'italic', 'strike', 'underline', 'color', 'font'],['ul', 'ol', 'checklist', 'code', 'link', 'image', 'table'],'fullscreen']展示在toolbar工具栏处的按钮,用[]包起来的表示是同一组,不同组的会有线隔开。也可以自定义,自定义时需要配置参数custom-toolbars
custom-toolbars{IToolbarItemConfig}--配置toolbar-config中对应按钮的具体设置 自定义工具栏

EditorMd 事件

事件名回调参数说明跳转 Demo
after-editor-initFunction(instance: object)编辑器初始化事件,返回编辑器对象
content-changeFunction(content: string)编辑器内容改变事件,返回当前内容
preview-content-changeFunction(string)预览内容改变时触发,返回对应的html字段
image-uploadFunction({file, callback})打开图片上传开关后,图片上传事件回调,返回文件内容与 callback 函数
checked-changeFunction(content: string)plugins添加checkbox后,预览checkbox checked状态改变回调

MdRender 参数

参数名类型默认值说明跳转 Demo
base-urlstring--设置渲染到 html 时,为相对 url 添加 baseUrl
custom-parse(html: string) => string--自定义对渲染后的 html 处理,需要接受渲染后的 html,返回自定义处理后的 html
md-rulesobject{}设置 markdown 对字符串的处理方式, 可参考markdown-it
custom-renderer-rulesICustomRenderRule[][]设置 markdown 对字符串的处理方式, 可参考markdown-it
custom-xss-rulesICustomXssRule[][]自定义 xss 对某种 tag 的过滤方式,每条规则需要指定 tag,并给出需要加入白名单的属性数组

MdRender 事件

事件名回调参数说明跳转 Demo
md-render-changeFunction(string)内容改变时触发,返回对应 html 渲染结果字段
checked-changeFunction(content: string)plugins添加checkbox后,预览checkbox checked状态改变回调

ICustomRenderRule

interface ICustomRenderRule {
  key: string;
  value: Function;
}

ICustomXssRule

interface ICustomXssRule {
  key: string;
  value: string[] | null;
}

MdPlugin

export interface MdPlugin {
  plugin: any;
  opts?: Object;
}

HintConfig

export interface HintConfigItem {
  handler: (obj: { callback: (replaceText: string) => void; cursorHint: string; prefix: string }) => void;
}
export interface HintConfig {
  throttleTime: number;  // 触发提示事件debounceTime(ms),默认300
  [key: string]: HintConfigItem;  // key为触发提示前缀配置
}

IToolbarItemConfig

export interface IToolbarItemConfig {
  id: string;
  name?: string;
  exitName?: string;
  type?: 'button' | 'dropDown';
  icon?: string;
  exitIcon?: string;
  template?: any;
  component?: any;
  shortKey?: string;
  params?: { [key: string]: any };
  handler?(editor?: any, params?: any): void;
}
const toolbars = Record<string, IToolbarItemConfig>