天天看點

vue內建editor.md

editor.md:一款markdown編輯器,個人這麼了解的,看起來很高大尚

官網位址:https://pandao.github.io/editor.md/

vue內建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>

           

這裡需要注意:我踩過的坑

  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" 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的話需要自己實作

vue內建editor.md

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

vue內建editor.md

編輯,需要在初始化的時候注冊插件

繼續閱讀