当我们的项目需要引入一个功能强大的markdown编辑器,我们兴许会考虑使用当今比较主流的markdown编辑器:editor.md,最重要的是它完全开源,免费。
这是它的官方地址:https://pandao.github.io/editor.md/
然而这个编辑器并不是拿来即用的,引入的时候可能会遇到一些棘手的问题和莫名其妙的错误,而且这些错误在网上百度却很难快速找到行之有效的解决方案。
写在前面
首先要注意的是,editor.md是依赖于 jQuery 的,所以必须先引入 jQuery 文件(或 jQuery对象),再引入editor.md的 js文件
解决editor.md图片上传问题
首先我们来看一下后端接口是怎么写的,后端使用的是java的springboot框架,写了一个文件上传的接口,它负责接收一个参数名为editormd-image-file
的文件,返回一个json字符串,这个json字符串也要满足editor.md所要求的:上传成功返回文件url路径和success状态码1,上传失败则返回success状态码0
:
/**
* 该方法值负责Editor.md的图片上传,因为Editor.md要求返回的json数据是固定格式 同时限制了上传文件的类型只可以是图片
* 并且为了访问起来方便,直接返回文件的绝对URL,后端把字符串拼接好,前端不用拼接字符串了
*
* @param file editormd-image-file 这是Editor.md上传文件固定的表单参数
* @param request 需要request对象用以动态生成web uri
* @return 返回的是文件的绝对URL,可直接定位到图片资源
*/
@LogRecord("Editor.md图片上传")
@ApiOperation("Editor.md图片上传")
@RequestMapping("/common/editor/md/upload/file")
public JSONObject uploadFileForEditor(@RequestParam(value = "editormd-image-file") MultipartFile file,
HttpServletRequest request) {
// response.setHeader("X-Frame-Options", "SAMEORIGIN");// 解决IFrame拒绝的问题
JSONObject res = new JSONObject();
if (file.isEmpty()) {
res.put("success", 0);// success=0表示上传失败
res.put("message", TourConstant.MESSAGE_FILE_CAN_NOT_BE_NULL);
return res;
}
String fileName = file.getOriginalFilename(); // 文件名
String suffixName = fileName.substring(fileName.lastIndexOf(".")); // 后缀名
if (!fileSuffixs.contains(suffixName)) {
res.put("success", 0);// success=0表示上传失败
res.put("message", TourConstant.MESSAGE_FILE_SUFFIX_NOT_ALLOW);
return res;
}
fileName = UUID.randomUUID().toString().replace("-", "").concat(suffixName);// 新文件名
String filePath = TourConstant.ATTR_UPLOAD_LOCATION; // 上传后的路径
String dateStr = dateFormat.format(new Date());
filePath = filePath.concat(dateStr).concat("\\");
File dest = new File(filePath + fileName);
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
try {
file.transferTo(dest.getAbsoluteFile());
String scheme = request.getScheme();
String serverName = request.getServerName();
String contextPath = request.getContextPath();
int serverPort = request.getServerPort();
String prefix = scheme + "://" + serverName + contextPath + ":" + serverPort
+ TourConstant.ATTR_UPLOAD_FILE_URI_PREFIX;
res.put("url", prefix + dateStr + "/" + fileName);
res.put("success", 1);
res.put("message", "upload success!");
return res;
} catch (IOException e) {
e.printStackTrace();
res.put("success", 0);// success=0表示上传失败
res.put("message", "upload failed!");
return res;
}
}
而我们下载好的editor.md,它的目录结构大致如下(下载解压后名字为editor.md-master,我把它重命名为了editormd):
我们只需要找到plugins
目录下的image-dialog
将其中的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 + ")");
console.log(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;
//console.log(formData);
$.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){
console.log(data.message);
dialog.find("[data-url]").val(data.url);
}else{
alert(data.message);
}
},
error: function (res) {
alert(res.responseText);
}
});
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);
}
})();
在引入该编辑器的时候,代码如下:
前提:已经引入了editor的所需的js和css文件
html部分:
<div class="layui-input-block" id="test-editor">
<textarea id="content" style="display:none;">...</textarea>
</div>
js部分:
editor = editormd("test-editor", {
width: 950,
height: 430,
path: "../../../editormd/lib/",
saveHTMLToTextarea: true,
searchReplace: true,
emoji: true,
imageUpload: true,
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
saveHTMLToTextarea: true, //注意:这个配置,方便post提交表单
crossDomainUpload: true,
imageUploadURL: layui.setter.http + "/common/editor/md/upload/file",
});
至此我相信文件上传的问题就能解决了。
解决editormd.js:2261 Uncaught TypeError: Cannot read property 'getValue' of undefined
这样的问题在什么时候会发生呢??
错误再现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../editor.md-master/css/editormd.css">
</head>
<body>
<div class="layui-input-block" id="test-editor">
<textarea id="content" style="display:none;">...</textarea>
</div>
</body>
<script src="../jquery/jquery-1.7.2.min.js"></script>
<script src="../editor.md-master/editormd.js"></script>
<script type="text/javascript">
$(function(){
let editor = editormd("test-editor", {
width: 950,
height: 430,
path: "../../../editor.md-master/lib/",
saveHTMLToTextarea: true,
});
console.log(editor.getMarkdown());
});
</script>
</html>
当声明了好editor 对象后,紧接着立马就调用它的getMarkdown()方法,就会报以上错误:
我们调用getMarkdown()方法, 而报错是getValue()未定义,看起来就很离谱。那肯定是getMarkdown()里面调用了getValue()这个方法,那既然没有报getMarkdown()未定义,说明该方法存在,该方法是人家editor.md开发人员写的,里面用到了getValue()方法,理论上getValue()肯定是定义好了,那为什么显示getValue()未定义呢?
解决:不要一开始立马就调用getMarkdown()方法,当editor对象创建好以后,让程序睡上500ms,再调用,就没事了。
( js学的不好,程序睡上500ms,只能用setTimeout()凑合凑合了)
$(function(){
let editor = editormd("test-editor", {
width: 950,
height: 430,
path: "../../../editor.md-master/lib/",
saveHTMLToTextarea: true,
});
setTimeout(function(){
console.log(editor.getMarkdown());
},500);
});
也是无语,对象中的函数居然是懒加载的,非得等上一会儿这个对象才能完全创建好。这种问题还是第一次见,真的不容易解决,百度不出解决方案,记录一下。
好了,关于使用editor.md所遇到的问题,就分享到这里。