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"></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);
    };

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

自定义渲染

自定义从 md 到 html 的渲染规则。
<template>
  <d-editor-md v-model="content" :custom-renderer-rules="customRendererRules"></d-editor-md>
</template>

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

export default defineComponent({
  setup() {
    const content = ref('[link](#test)');
    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 };
  },
});
</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: any) => {
          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>

配置快速提示

设置 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 图片功能上传,传出事件回调)

EditorMd 事件

事件名回调参数说明跳转 Demo
after-editor-initFunction(instance: object)编辑器初始化事件,返回编辑器对象
content-changeFunction(content: string)编辑器内容改变事件,返回当前内容
preview-content-changeFunction()预览内容改变时触发
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[];
}

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为触发提示前缀配置
}