editor.md:一款markdown編輯器,個人這麼了解的,看起來很高大尚
官網位址:https://pandao.github.io/editor.md/
坑爹的是在vue裡面不能直接使用,需要自己封裝
markdown一般需要兩個東西,編輯和預覽,分享一個我自己基于editor.md封裝的元件,用的是vue3
錄屏就沒有了,動圖的話太大上傳不了
源碼位址
-
編輯
https://gitee.com/qiudw/platform/tree/master/platform-admin-web/src/main/front-end/src/components/EditorMarkdown
-
預覽
https://gitee.com/qiudw/platform/tree/master/platform-admin-web/src/main/front-end/src/components/EditormdPreview
這裡需要安裝scriptjs
npm install scriptjs --save
編輯元件
<template>
<div>
<link rel="stylesheet" href="/static/editor.md/css/editormd.css" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >
<!-- editormd -->
<div id="editor" style="z-index: 10" />
</div>
</template>
<script>
import scriptjs from 'scriptjs'
export default {
name: 'EditorMarkdown',
props: {
modelValue: {
type: String,
required: false,
default: ''
},
height: {
type: String,
required: false,
default: '600px'
}
},
data() {
return {
editor: {}
}
},
mounted() {
// 設定延遲初始化markdown編輯器, 因為隻會初始化一次,需要等待資料加載完成之後再初始化
setTimeout(() => {
this.initEditor()
}, 300)
},
methods: {
initEditor() {
(async() => {
await this.fetchScript('/static/editor.md/jquery-1.11.3.min.js')
await this.fetchScript('/static/editor.md/editormd.min.js')
this.$nextTick(() => {
// 内容
var content = this.modelValue
const editor = window.editormd('editor', {
path: '/static/editor.md/lib/',
height: this.height,
emoji: true,
// 開啟圖檔上傳,圖檔上傳重寫了的
imageUpload: true,
imageFormats: ['jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'],
// 這裡需要考慮傳回值,是以封裝了一層
imageUploadURL: '/markdown/upload',
htmlDecode: true, // 識别html标簽
// 監聽更新,更新父元件值
change: function() {
this.$emit('update:modelValue', this.getMarkdown())
},
// 退出全屏
onfullscreen: function() {
// 原生JS修改層級
var editor = document.getElementById('editor')
editor.style['z-index'] = 13
},
// 全屏
onfullscreenExit: function() {
// 原生JS修改層級
var editor = document.getElementById('editor')
editor.style['z-index'] = 10
},
// 加載完成後再設定内容
onload: function() {
this.setMarkdown(content)
// 加載ctrl + v粘貼圖檔插件
window.editormd.loadPlugin('/static/editor.md/plugins/image-handle-paste/image-handle-paste', function() {
editor.imagePaste()
})
}
})
const vm = this
// 監聽,改變父元件的值
editor.on('change', function() {
vm.$emit('update:modelValue', this.getMarkdown())
})
this.editor = editor
})
})()
},
fetchScript(url) {
return new Promise((resolve) => {
scriptjs(url, () => {
resolve()
})
})
}
}
}
</script>
<style scoped>
/* 上傳圖檔彈窗樣式有點問題,可能是沖突了 */
#editor::v-deep(.editormd-dialog-header) {
padding: 0 20px;
}
</style>
這裡需要注意:我踩過的坑
- editor對象
,data裡面儲存後,其他方法裡面this.editor = editor
不是this.editor
建立的那個,是以setValue(), setMarkdown()方法都用不了editormd('', options)
-
全屏壓蓋
z-index層級問題,我這裡的解決方法是監聽editor的全屏和退出全屏事件,js修改z-index樣式
-
更新回報
監聽editor的change事件,将值回報給父元件,父元件通過
綁定值,這裡修改父元件的值是不是更新到子元件額v-model
-
初始化延遲
更新的時候,一般都是異步加載,由于editor隻初始化一次的問題,是以這裡設定延遲初始化
調用
<EditorMarkdown v-model="form.content" />
預覽元件
<template>
<div>
<link rel="stylesheet" href="/static/editor.md/css/editormd.css" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >
<div id="editor" style="padding: 0">
<textarea id="content" v-model="markdownToHtml" />
</div>
</div>
</template>
<script>
import scriptjs from 'scriptjs'
export default {
name: 'EditormdPreview',
props: {
value: {
type: String,
required: false,
default: ''
}
},
data() {
return {
editor: null
}
},
computed: {
markdownToHtml() {
return this.value
}
},
mounted() {
// 初始化
this.initEditor()
},
methods: {
initEditor() {
(async() => {
await this.fetchScript('/static/editor.md/jquery-1.11.3.min.js')
await this.fetchScript('/static/editor.md/lib/marked.min.js')
await this.fetchScript('/static/editor.md/lib/prettify.min.js')
await this.fetchScript('/static/editor.md/lib/raphael.min.js')
await this.fetchScript('/static/editor.md/lib/underscore.min.js')
await this.fetchScript('/static/editor.md/lib/sequence-diagram.min.js')
await this.fetchScript('/static/editor.md/lib/flowchart.min.js')
await this.fetchScript('/static/editor.md/lib/jquery.flowchart.min.js')
await this.fetchScript('/static/editor.md/editormd.min.js')
await this.$nextTick(() => {
this.editor = window.editormd.markdownToHTML('editor', {
path: '/static/editor.md/lib/',
emoji: true,
htmlDecode: true // 識别html标簽
})
})
// const content = this.value
// // 設定值, 另一種方法
// const contentDoc = document.getElementById('content')
// contentDoc.value = content
})()
},
fetchScript(url) {
return new Promise((resolve) => {
scriptjs(url, () => {
resolve()
})
})
}
}
}
</script>
<style scoped>
</style>
調用
<EditormdPreview :value="markdown.content" />
圖檔上傳
這裡需要重寫上傳,這裡上傳是不帶token的,是以需要加到白名單裡面去,要求攜帶token的話需要自己實作
image-dialog.js,這是從網上找的,可以用
/*!
* Image (upload) dialog plugin for Editor.md
*
* @file image-dialog.js
* @author pandao
* @version 1.3.4
* @updateTime 2015-06-09
* {@link https://github.com/pandao/editor.md}
* @license MIT
*/
(function() {
var factory = function (exports) {
var pluginName = "image-dialog";
exports.fn.imageDialog = function () {
var _this = this;
var cm = this.cm;
var lang = this.lang;
var editor = this.editor;
var settings = this.settings;
var cursor = cm.getCursor();
var selection = cm.getSelection();
var imageLang = lang.dialog.image;
var classPrefix = this.classPrefix;
var iframeName = classPrefix + "image-iframe";
var dialogName = classPrefix + pluginName, dialog;
cm.focus();
var loading = function (show) {
var _loading = dialog.find("." + classPrefix + "dialog-mask");
_loading[(show) ? "show" : "hide"]();
};
if (editor.find("." + dialogName).length < 1) {
var guid = (new Date).getTime();
var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid;
if (settings.crossDomainUpload) {
action += "&callback=" + settings.uploadCallbackURL + "&dialog_id=editormd-image-dialog-" + guid;
}
//注釋的是官方的寫法
// var dialogContent = ( (settings.imageUpload) ? "<form action=\"" + action +"\" target=\"" + iframeName + "\" method=\"post\" enctype=\"multipart/form-data\" class=\"" + classPrefix + "form\">" : "<div class=\"" + classPrefix + "form\">" ) +
// ( (settings.imageUpload) ? "<iframe name=\"" + iframeName + "\" id=\"" + iframeName + "\" guid=\"" + guid + "\"></iframe>" : "" ) +
// "<label>" + imageLang.url + "</label>" +
// "<input type=\"text\" data-url />" + (function(){
// return (settings.imageUpload) ? "<div class=\"" + classPrefix + "file-input\">" +
// "<input type=\"file\" name=\"" + classPrefix + "image-file\" accept=\"image/*\" />" +
// "<input type=\"submit\" value=\"" + imageLang.uploadButton + "\" />" +
// "</div>" : "";
// })() +
// "<br/>" +
// "<label>" + imageLang.alt + "</label>" +
// "<input type=\"text\" value=\"" + selection + "\" data-alt />" +
// "<br/>" +
// "<label>" + imageLang.link + "</label>" +
// "<input type=\"text\" value=\"http://\" data-link />" +
// "<br/>" +
// ( (settings.imageUpload) ? "</form>" : "</div>");
//這是我個人寫法
var dialogContent = ((settings.imageUpload) ? "<form action=\"#\" target=\"" + iframeName + "\" method=\"post\" enctype=\"multipart/form-data\" class=\"" + classPrefix + "form\">" : "<div class=\"" + classPrefix + "form\">") +
((settings.imageUpload) ? "<iframe name=\"" + iframeName + "\" id=\"" + iframeName + "\" guid=\"" + guid + "\"></iframe>" : "") +
"<label>" + imageLang.url + "</label>" +
"<input type=\"text\" data-url />" + (function () {
return (settings.imageUpload) ? "<div class=\"" + classPrefix + "file-input\">" +
"<input type=\"file\" name=\"" + classPrefix + "image-file\" id=\"" + classPrefix + "image-file\" accept=\"image/*\" />" +
"<input type=\"submit\" value=\"" + imageLang.uploadButton + "\" />" +
"</div>" : "";
})() +
"<br/>" +
"<label>" + imageLang.alt + "</label>" +
"<input type=\"text\" value=\"" + selection + "\" data-alt />" +
"<br/>" +
"<label>" + imageLang.link + "</label>" +
"<input type=\"text\" value=\"http://\" data-link />" +
"<br/>" +
((settings.imageUpload) ? "</form>" : "</div>");
//這是官方的,不知道為什麼,官方把它給注釋掉了
//var imageFooterHTML = "<button class=\"" + classPrefix + "btn " + classPrefix + "image-manager-btn\" style=\"float:left;\">" + imageLang.managerButton + "</button>";
dialog = this.createDialog({
title: imageLang.title,
width: (settings.imageUpload) ? 465 : 380,
height: 254,
name: dialogName,
content: dialogContent,
mask: settings.dialogShowMask,
drag: settings.dialogDraggable,
lockScreen: settings.dialogLockScreen,
maskStyle: {
opacity: settings.dialogMaskOpacity,
backgroundColor: settings.dialogMaskBgColor
},
buttons: {
enter: [lang.buttons.enter, function () {
var url = this.find("[data-url]").val();
var alt = this.find("[data-alt]").val();
var link = this.find("[data-link]").val();
if (url === "") {
alert(imageLang.imageURLEmpty);
return false;
}
var altAttr = (alt !== "") ? " \"" + alt + "\"" : "";
if (link === "" || link === "http://") {
cm.replaceSelection("![" + alt + "](" + url + altAttr + ")");
} else {
cm.replaceSelection("[![" + alt + "](" + url + altAttr + ")](" + link + altAttr + ")");
}
if (alt === "") {
cm.setCursor(cursor.line, cursor.ch + 2);
}
this.hide().lockScreen(false).hideMask();
//删除對話框
this.remove();
return false;
}],
cancel: [lang.buttons.cancel, function () {
this.hide().lockScreen(false).hideMask();
//删除對話框
this.remove();
return false;
}]
}
});
dialog.attr("id", classPrefix + "image-dialog-" + guid);
if (!settings.imageUpload) {
return;
}
var fileInput = dialog.find("[name=\"" + classPrefix + "image-file\"]");
fileInput.bind("change", function () {
var fileName = fileInput.val();
var isImage = new RegExp("(\\.(" + settings.imageFormats.join("|") + "))$", "i"); // /(\.(webp|jpg|jpeg|gif|bmp|png))$/
if (fileName === "") {
alert(imageLang.uploadFileEmpty);
return false;
}
if (!isImage.test(fileName)) {
alert(imageLang.formatNotAllowed + settings.imageFormats.join(", "));
return false;
}
loading(true);
var submitHandler = function () {
var uploadIframe = document.getElementById(iframeName);
uploadIframe.onload = function () {
loading(false);
//注釋的是官方寫法
// var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
// var json = (body.innerText) ? body.innerText : ( (body.textContent) ? body.textContent : null);
//
// json = (typeof JSON.parse !== "undefined") ? JSON.parse(json) : eval("(" + json + ")");
//
// if(!settings.crossDomainUpload)
// {
// if (json.success === 1)
// {
// dialog.find("[data-url]").val(json.url);
// }
// else
// {
// alert(json.message);
// }
// }
//
// return false;
//這是我個人寫法
var formData = new FormData();
formData.append("editormd-image-file", $("#editormd-image-file")[0].files[0]);
var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid;
$.ajax({
type: "post",
url: action,
data: formData,
dataType: "json",
async: false,
processData: false, // 使資料不做處理
contentType: false, // 不要設定Content-Type請求頭
success: function (data) {
// 成功拿到結果放到這個函數 data就是拿到的結果
if (data.success === "1") {
dialog.find("[data-url]").val(data.url);
} else {
alert(data.message);
}
},
});
return false;
};
};
dialog.find("[type=\"submit\"]").bind("click", submitHandler).trigger("click");
});
}
dialog = editor.find("." + dialogName);
dialog.find("[type=\"text\"]").val("");
dialog.find("[type=\"file\"]").val("");
dialog.find("[data-link]").val("http://");
this.dialogShowMask(dialog);
this.dialogLockScreen();
dialog.show();
};
};
// CommonJS/Node.js
if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
module.exports = factory;
} else if (typeof define === "function") // AMD/CMD/Sea.js
{
if (define.amd) { // for Require.js
define(["editormd"], function (editormd) {
factory(editormd);
});
} else { // for Sea.js
define(function (require) {
var editormd = require("./../../editormd");
factory(editormd);
});
}
} else {
factory(window.editormd);
}
})();
服務端接收與傳回值
/**
* 檔案上傳
* @param multipartFile 上傳的檔案對象
* @return editormd格式的結果
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public JSONObject upload(@RequestParam(value = "editormd-image-file") MultipartFile multipartFile) {
System.out.println(multipartFile.getOriginalFilename());
Calendar calendar = Calendar.getInstance();
String path = "/" + calendar.get(Calendar.YEAR) + "/" + (calendar.get(Calendar.MONTH) + 1) + "/" + calendar.get(Calendar.DAY_OF_MONTH);
// 上傳結果
ResultDto<String> upload = fileRemoteService.upload(path, multipartFile, true);
JSONObject jsonObject = new JSONObject();
jsonObject.put("success", "1");
jsonObject.put("message", "上傳成功");
jsonObject.put("url", "/file" + upload.getData());
return jsonObject;
}
ctrl + v圖檔上傳
image-handle-paste.js
編輯,需要在初始化的時候注冊插件