editor.md:一款markdown编辑器,个人这么理解的,看起来很高大尚
官网地址:https://pandao.github.io/editor.md/

截图来自官网

坑爹的是在vue里面不能直接使用,需要自己封装

markdown一般需要两个东西,编辑和预览,分享一个我自己基于editor.md封装的组件,用的是vue3

录屏就没有了,动图的话太大上传不了

源码地址

这里需要安装scriptjs

npm install scriptjs --save

编辑组件

<template>
  <div>
    <link rel="stylesheet" href="/static/editor.md/css/editormd.css">
    <!-- 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>

这里需要注意:我踩过的坑

  1. editor对象
    this.editor = editor,data里面保存后,其他方法里面this.editor不是editormd('', options)创建的那个,所以setValue(), setMarkdown()方法都用不了

  2. 全屏压盖
    z-index层级问题,我这里的解决方法是监听editor的全屏和退出全屏事件,js修改z-index样式

  3. 更新反馈
    监听editor的change事件,将值反馈给父组件,父组件通过v-model绑定值,这里修改父组件的值是不是更新到子组件额

  4. 初始化延迟
    更新的时候,一般都是异步加载,由于editor只初始化一次的问题,所以这里设置延迟初始化

调用

<EditorMarkdown v-model="form.content" />

预览组件

<template>
  <div>
    <link rel="stylesheet" href="/static/editor.md/css/editormd.css">
    <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
在这里插入图片描述
编辑,需要在初始化的时候注册插件

Logo

前往低代码交流专区

更多推荐