Markdown MD 에디터
Markdown을 편집하면서 내용을 바로 렌더링까지 확인할 수 있는 에디터 컴포넌트예요.
출처: 문서
본문
기본 사용법
:::demo 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>
:::
사용자 지정 렌더링
:::demo 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>
:::
사용자 지정 툴바
:::demo 에디터 툴바를 직접 구성해요.
<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>
:::
순수 렌더링 모드
:::demo 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>
:::
단일 컬럼 렌더링 모드
:::demo 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>
:::
이미지 파일 업로드 설정
:::demo 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 렌더링
:::demo md-plugins에 checkbox 플러그인을 설정하면 checkbox를 렌더링하고 checked 변경에 반응해요.
editor-md/checkbox
:::
수학 공식 렌더링
:::demo 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 렌더링
:::demo 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 이모티콘을 렌더링해요. 자세한 사용 방법은 예제 코드를 참고하세요.
:::demo
<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>
:::
빠른 힌트 설정
:::demo 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 목차 렌더링
:::demo 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 렌더링
:::demo 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-model | string |
'' | 에디터 내용을 양방향 바인딩해요 |
| options | object |
{} | 에디터 초기화 시 적용할 커스텀 설정이에요. CodeMirror Options를 참고하세요 |
| base-url | string |
-- | html로 렌더링할 때 상대 url 앞에 붙일 baseUrl을 설정해요 |
| custom-parse | (html: string) => string |
-- | 렌더링된 html을 직접 처리해요. 렌더링된 html을 받아서 커스텀 처리 후의 html을 반환해야 해요 |
| md-rules | object |
{} | markdown이 문자열을 처리하는 방식을 설정해요. markdown-it을 참고하세요 |
| md-plugins | MdPlugin[] | -- | markdown-it 플러그인을 설정해요 |
| hintConfig | MdHintConfig[] | -- | 빠른 힌트 설정을 구성해요 |
| mode | 'editonly' | 'readonly' | 'normal' |
'normal' | 쓰기 전용/읽기 전용/두 컬럼 표시 모드를 고를 수 있어요. 기본값인 'normal'은 두 컬럼 모드로 표시해요 |
| custom-renderer-rules | ICustomRenderRule[] | [] | markdown이 노드를 렌더링하는 방식을 직접 정의해요. 각 규칙은 대응하는 노드 key를 지정하고 렌더링 함수를 직접 작성해야 해요 |
| custom-xss-rules | ICustomXssRule[] | [] | xss가 특정 tag를 필터링하는 방식을 직접 정의해요. 각 규칙은 tag를 지정하고 화이트리스트에 넣을 속성 배열을 제공해야 해요 |
| placeholder | string |
'' | 에디터에 내용이 없을 때 표시되는 안내 문구예요 |
| fullscreen-z-index | number |
10 | 에디터가 전체 화면 상태일 때의 z-index예요 |
| image-upload-to-server | boolean |
false | 이미지 커스텀 업로드 스위치를 켤지 여부예요 (켜면 이미지 복사를 감시하고, 툴바의 이미지 기능으로 업로드할 때 이벤트 콜백을 내보내요) |
| editor-container-height | number |
-- | 선택, 에디터 내용 영역의 높이예요 |
| toolbar-config | Array(string) |
[['undo', 'redo'],['h1', 'h2', 'bold', 'italic', 'strike', 'underline', 'color', 'font'],['ul', 'ol', 'checklist', 'code', 'link', 'image', 'table'],'fullscreen'] |
툴바에 표시할 버튼이에요. []로 감싸면 같은 그룹이라는 뜻이고, 그룹이 다르면 선으로 구분돼요. 커스텀도 가능하며, 이때는 custom-toolbars 파라미터를 함께 설정해야 해요 |
| custom-toolbars | {IToolbarItemConfig} | -- | toolbar-config에 있는 각 버튼의 세부 설정을 구성해요 사용자 지정 툴바 |
EditorMd 이벤트
| 이벤트명 | 콜백 파라미터 | 설명 | Demo 링크 |
|---|---|---|---|
| after-editor-init | Function(instance: object) |
에디터 초기화 이벤트예요. 에디터 객체를 반환해요 | |
| content-change | Function(content: string) |
에디터 내용 변경 이벤트예요. 현재 내용을 반환해요 | |
| preview-content-change | Function(string) |
미리보기 내용이 바뀔 때 발생해요. 대응하는 html 필드를 반환해요 | |
| image-upload | Function({file, callback}) |
이미지 업로드 스위치를 켠 뒤 발생하는 이미지 업로드 이벤트 콜백이에요. 파일 내용과 callback 함수를 반환해요 | |
| checked-change | Function(content: string) |
plugins에 checkbox를 추가한 뒤, 미리보기의 checkbox checked 상태가 바뀔 때의 콜백이에요 |
MdRender 파라미터
| 파라미터 | 타입 | 기본값 | 설명 | Demo 링크 |
|---|---|---|---|---|
| base-url | string |
-- | html로 렌더링할 때 상대 url 앞에 붙일 baseUrl을 설정해요 | |
| custom-parse | (html: string) => string |
-- | 렌더링된 html을 직접 처리해요. 렌더링된 html을 받아서 커스텀 처리 후의 html을 반환해야 해요 | |
| md-rules | object |
{} | markdown이 문자열을 처리하는 방식을 설정해요. markdown-it을 참고하세요 | |
| custom-renderer-rules | ICustomRenderRule[] | [] | markdown이 노드를 렌더링하는 방식을 직접 정의해요. 각 규칙은 대응하는 노드 key를 지정하고 렌더링 함수를 직접 작성해야 해요 | |
| custom-xss-rules | ICustomXssRule[] | [] | xss가 특정 tag를 필터링하는 방식을 직접 정의해요. 각 규칙은 tag를 지정하고 화이트리스트에 넣을 속성 배열을 제공해야 해요 |
MdRender 이벤트
| 이벤트명 | 콜백 파라미터 | 설명 | Demo 링크 |
|---|---|---|---|
| md-render-change | Function(string) |
내용이 바뀔 때 발생해요. 대응하는 html 렌더링 결과 필드를 반환해요 | |
| checked-change | Function(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>
더 알아보기 (Learn more)
d-editor-md는 Markdown 편집부터 katex·mermaid 같은 플러그인 확장, 이미지 업로드까지 폭넓게 지원해요. 먼저 기본 사용법으로 v-model 바인딩에 익숙해진 뒤 필요한 기능을 하나씩 붙여 보세요.