Browse Source

自定义富文本编辑器

Michael Wang 4 years ago
parent
commit
6ce93c4e60

+ 69 - 0
src/components/VEditor/VEditor.vue

@@ -0,0 +1,69 @@
+<template>
+  <div>
+    <VMenu />
+    <div
+      :id="'ved' + _uid"
+      ref="editor"
+      class="v-editor"
+      :data-placeholder="placeholder"
+      contenteditable
+      @blur="emitJSON"
+    ></div>
+  </div>
+</template>
+
+<script>
+import VMenu from "./components/VMenu.vue";
+import { renderRichText } from "./renderJSON";
+import { toJSON } from "./toJSON";
+
+export default {
+  name: "VEditor",
+  components: {
+    VMenu,
+  },
+  props: {
+    placeholder: { type: String, default: "请输入..." },
+    value: {
+      type: String,
+      default: () => "{}",
+    },
+  },
+  watch: {
+    value() {
+      renderRichText(JSON.parse(this.value), this.$refs.editor);
+    },
+  },
+  mounted() {
+    renderRichText(JSON.parse(this.value), this.$refs.editor);
+  },
+  methods: {
+    emitJSON() {
+      if (this.$refs.editor.contentEditable) {
+        const json = toJSON(this.$refs.editor);
+        this.$emit("input", json);
+        this.$emit("change", json);
+      }
+    },
+  },
+};
+</script>
+
+<style>
+.v-editor,
+.sourceView {
+  border: 1px solid grey;
+  border-radius: 5px;
+  height: 300px;
+  padding: 5px;
+  overflow: scroll;
+}
+
+.sourceView {
+  margin: -5px;
+}
+
+.v-editor[contenteditable="true"]:empty:not(:focus)::before {
+  content: attr(data-placeholder);
+}
+</style>

+ 65 - 0
src/components/VEditor/changeMode.js

@@ -0,0 +1,65 @@
+const { toJSON } = require("./toJSON");
+
+let bToSource = false;
+let bToJSON = false;
+
+/**
+ *
+ * @param {string} type
+ * @param {HTMLDivElement} edt
+ */
+export function setDocMode(type, edt) {
+  if (type === "HTML") {
+    // if (bToSource === true) return;
+    bToJSON = false;
+    bToSource = !bToSource;
+  }
+  if (type === "JSON") {
+    if (bToSource === true) return;
+    bToSource = false;
+    bToJSON = !bToJSON;
+  }
+
+  let oContent;
+  if (bToSource) {
+    oContent = document.createTextNode(edt.innerHTML);
+    edt.innerHTML = "";
+    edt.contentEditable = false;
+    let oPre = document.createElement("pre");
+
+    oPre.className = "sourceView";
+    oPre.contentEditable = true;
+    oPre.style = "overflow: scroll; white-space: normal;";
+    oPre.appendChild(oContent);
+    edt.appendChild(oPre);
+    // 似乎chrome不支持将默认分段改为p
+    document.execCommand("defaultParagraphSeparator", false, "div");
+  } else if (bToJSON) {
+    const je = document.createElement("div");
+    je.style =
+      "position: absolute; top: 0; left: 0; background: beige; width: 100%; padding: 5px; z-index: 1000000";
+
+    oContent = document.createTextNode(toJSON(edt));
+
+    let oPre = document.createElement("pre");
+
+    // oPre.className = "sourceView";
+    edt.contentEditable = false;
+    oPre.style = "overflow: scroll;";
+    oPre.appendChild(oContent);
+    oPre.innerHTML = "双击关闭\n\n\n" + oPre.innerHTML;
+    je.appendChild(oPre);
+    document.body.appendChild(je);
+
+    je.addEventListener("dblclick", () => {
+      document.body.removeChild(je);
+      bToJSON = false;
+    });
+  } else {
+    oContent = document.createRange();
+    oContent.selectNodeContents(edt.firstChild);
+    edt.innerHTML = oContent.toString();
+    edt.contentEditable = true;
+  }
+  edt.focus();
+}

+ 54 - 0
src/components/VEditor/components/VMenu.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="edit-menus" style="display: flex; gap: 10px;">
+    <img
+      class="intLink"
+      title="Undo"
+      @click="execCommand('undo')"
+      src="data:image/gif;base64,R0lGODlhFgAWAOMKADljwliE33mOrpGjuYKl8aezxqPD+7/I19DV3NHa7P///////////////////////yH5BAEKAA8ALAAAAAAWABYAAARR8MlJq7046807TkaYeJJBnES4EeUJvIGapWYAC0CsocQ7SDlWJkAkCA6ToMYWIARGQF3mRQVIEjkkSVLIbSfEwhdRIH4fh/DZMICe3/C4nBQBADs="
+    />
+    <img
+      class="intLink"
+      title="Bold"
+      @click="execCommand('bold')"
+      src="data:image/gif;base64,R0lGODlhFgAWAID/AMDAwAAAACH5BAEAAAAALAAAAAAWABYAQAInhI+pa+H9mJy0LhdgtrxzDG5WGFVk6aXqyk6Y9kXvKKNuLbb6zgMFADs="
+    />
+    <img
+      class="intLink"
+      title="Italic"
+      @click="execCommand('italic')"
+      src="data:image/gif;base64,R0lGODlhFgAWAKEDAAAAAF9vj5WIbf///yH5BAEAAAMALAAAAAAWABYAAAIjnI+py+0Po5x0gXvruEKHrF2BB1YiCWgbMFIYpsbyTNd2UwAAOw=="
+    />
+    <img
+      class="intLink"
+      title="Underline"
+      @click="execCommand('underline')"
+      src="data:image/gif;base64,R0lGODlhFgAWAKECAAAAAF9vj////////yH5BAEAAAIALAAAAAAWABYAAAIrlI+py+0Po5zUgAsEzvEeL4Ea15EiJJ5PSqJmuwKBEKgxVuXWtun+DwxCCgA7"
+    />
+    <!-- <span class="intLink" title="显示源码" @click="setDocMode('HTML')">
+      &lt;/&gt;
+    </span> -->
+    <span class="intLink" @click="setDocMode('JSON')" title="to JSON">{ }</span>
+  </div>
+</template>
+
+<script>
+import { setDocMode } from "../changeMode";
+
+export default {
+  name: "VMenu",
+  methods: {
+    setDocMode(type) {
+      setDocMode(type, this.$parent.$refs.editor);
+    },
+    execCommand(command) {
+      document.execCommand(command);
+    },
+  },
+};
+</script>
+
+<style>
+.intLink {
+  cursor: default;
+}
+</style>

File diff suppressed because it is too large
+ 17 - 0
src/components/VEditor/constants.js


+ 8 - 0
src/components/VEditor/main.js

@@ -0,0 +1,8 @@
+import Vue from "vue";
+import App from "./App.vue";
+
+Vue.config.productionTip = false;
+
+new Vue({
+  render: (h) => h(App),
+}).$mount("#app");

+ 76 - 0
src/components/VEditor/renderJSON.js

@@ -0,0 +1,76 @@
+// const _text_styles_ = ["bold", "underline", "italic", "sup", "sub"];
+
+export function renderRichText(body, container) {
+  let sections = body.sections || [];
+  let nodes = [];
+  sections.forEach((section) => {
+    nodes.push(renderSection(section));
+  });
+  if (container != undefined) {
+    // container.classList.add("rich-text");
+    while (container.hasChildNodes()) {
+      container.removeChild(container.lastChild);
+    }
+    nodes.forEach((node) => {
+      container.appendChild(node);
+    });
+  }
+
+  return nodes;
+}
+
+function renderSection(section) {
+  let blocks = section.blocks || [];
+  let inline = blocks.length > 1;
+  let node = document.createElement("div");
+  blocks.forEach((block) => {
+    node.appendChild(renderBlock(block, inline));
+  });
+  return node;
+}
+
+function renderBlock(block, inline) {
+  // let node = document.createElement('span')
+  // let classList = node.classList
+  let node;
+  if (block.type === "text") {
+    // classList.add('text')
+    // if (block.param != undefined) {
+    //     _text_styles_.forEach(style => {
+    //         if (block.param[style] === true) {
+    //             classList.add(style)
+    //         }
+    //     })
+    // }
+    if (block.param) {
+      if (block.param.underline) {
+        node = document.createElement("u");
+      } else if (block.param.bold) {
+        node = document.createElement("b");
+      } else if (block.param.italic) {
+        node = document.createElement("i");
+      }
+      node.textContent = block.value;
+    } else {
+      node = document.createTextNode(block.value);
+    }
+  } else if (block.type === "image") {
+    // classList.add("image");
+    // classList.add("loading");
+    node = document.createElement("img");
+    if (inline === true) {
+      node.classList.add("inline");
+    }
+    node.src = block.value;
+    // img.onload = function () {
+    //   this.parentNode.classList.remove("loading");
+    // };
+  } else if (block.type === "audio") {
+    // classList.add("audio");
+    // let audio = node.appendChild(new Audio());
+    // audio.src = block.value;
+    // audio.controls = true;
+  }
+
+  return node;
+}

+ 89 - 0
src/components/VEditor/toJSON.js

@@ -0,0 +1,89 @@
+let res = [];
+
+/**
+ *
+ * @param {HTMLDivElement} editor
+ */
+export function toJSON(editor) {
+  // console.log(editor.innerHTML);
+  res = [];
+  for (const e of [...editor.childNodes]) {
+    toSection(e);
+  }
+  const newRes = [];
+  for (let s of res) {
+    let newS = [];
+    for (const b of s) {
+      const toB = toBlock(b);
+      if (toB) newS.push(toB);
+    }
+    if (newS.length === 0) {
+      // 空行特殊处理
+      newS = [{ type: "text", value: " ", param: null }];
+    }
+    newRes.push({ blocks: newS });
+  }
+  res = { sections: newRes };
+  // console.log(res);
+  // console.log(JSON.stringify(res));
+  // console.log(JSON.stringify(res, null, 2));
+  return JSON.stringify(res, null, 2);
+}
+
+/**
+ *
+ * @param {Node} e
+ */
+function toSection(e) {
+  if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "DIV") {
+    res.push(e.childNodes);
+  } else if (e.nodeType == Node.TEXT_NODE) {
+    res.push([e]);
+  } else {
+    console.log("toSection: 非div, 非TEXT", e);
+  }
+}
+
+/**
+ *
+ * @param {Node} e
+ */
+function toBlock(e) {
+  let block = {};
+  if (e.nodeType === Node.TEXT_NODE) {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = null;
+    // } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "SPAN") {
+    //   block.type = "text";
+    //   block.value = e.textContent;
+    //   block.param = {};
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "U") {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = { underline: true };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "B") {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = { bold: true };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "I") {
+    block.type = "text";
+    block.value = e.textContent;
+    block.param = { italic: true };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "IMG") {
+    block.type = "image";
+    block.value = e.src;
+    block.param = { width: e.width, height: e.height };
+  } else if (e.nodeType == Node.ELEMENT_NODE && e.nodeName === "BR") {
+    block.type = "text";
+    block.value = "";
+    block.param = null;
+  } else {
+    console.log("toBlock: 非法", e);
+  }
+
+  if (block.type === "text" && block.value === "") {
+    block = null; // 返回null的block,从json中剔除
+  }
+  return block;
+}

+ 2 - 0
src/components/registerComponents.js

@@ -1,6 +1,8 @@
 import Vue from "vue";
 // import upperFirst from "lodash/upperFirst";
 // import camelCase from "lodash/camelCase";
+import VEditor from "./VEditor/VEditor";
+Vue.component("VEditor", VEditor);
 
 const requireComponent = require.context(
   // The relative path of the components folder

+ 2 - 2
src/features/examwork/ExamManagement/ExamEdit.vue

@@ -332,7 +332,7 @@
         <el-form :model="form" label-width="170px" inline>
           <el-row>
             <el-form-item label="考试须知">
-              <el-input v-model.trim="form.preNotice"></el-input>
+              <VEditor v-model="form.preNotice" style="width: 300px;" />
             </el-form-item>
           </el-row>
           <el-row>
@@ -344,7 +344,7 @@
           </el-row>
           <el-row>
             <el-form-item label="考后说明">
-              <el-input v-model.trim="form.postNotice"></el-input>
+              <VEditor v-model="form.postNotice" style="width: 300px;" />
             </el-form-item>
           </el-row>
           <el-row>

Some files were not shown because too many files changed in this diff