wangEditor5 customizes the menu bar in vue–format brush, upload pictures, video functions

Copy, paste, modify and use it directly, and also write relevant comments.

1. Install related plug-ins

npm install @wangeditor/editor
npm install @wangeditor/editor-for-vue

2. Official key documents

  1. ButtonMenu: https://www.wangeditor.com/v5/development.html#buttonmenu
  2. Register menu to wangEditor: Customize new extension features | wangEditor
  3. insertKeys keys for custom functions: https://www.wangeditor.com/v5/toolbar-config.html#insertkeys
  4. Custom upload image and video function: menu configuration | wangEditor
  5. Source code address: GitHub – wangeditor-team/wangEditor: wangEditor – open source web rich text editor

3. Initialize editor (wangEdit.vue)

<template>
  <div style="border: 1px solid #ccc">
    <Toolbar
      style="border-bottom: 1px solid #ccc"
      :editor="editor"
      :defaultConfig="toolbarConfig"
      :mode="mode"
    />
    <Editor
      style="height: 500px; overflow-y: hidden"
      v-model="html"
      :defaultConfig="editorConfig"
      :mode="mode"
      @onCreated="onCreated"
      @onChange="onChange"
    />
  </div>
</template>

<script>
// import Location from "@/utils/location";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { Boot, IModuleConf, DomEditor } from "@wangeditor/editor";
import { getToken } from "@/utils/auth";
import MyPainter from "./geshi";
const menu1Conf = {
  key: "geshi", // Define menu key: ensure uniqueness and non-duplication (important)
  factory() {
    return new MyPainter(); // Replace `YourMenuClass` with your menu’s class
  },
};
const module = {
  // JS syntax
  menus: [menu1Conf],

  //Other functions are explained below...
};
Boot.registerModule(module);
export default {
  components: { Editor, Toolbar },
  props: {
    relationKey: {
      type: String,
      default: "",
    },
  },
  created() {
    console.log(this.editorConfig.MENU_CONF.uploadImage.meta.activityKey);
  },
  data() {
    return {
      //Rich text instance
      editor: null,
      //Rich text content
      html: "",
      // Editor mode
      mode: "default", // or 'simple'
      // Toolbar configuration
      toolbarConfig: {
        //Add menu
        insertKeys: {
          index: 32,
          keys: ["geshi"],
        },
        //Remove images and videos uploaded over the Internet
        excludeKeys: ["insertImage", "insertVideo"],
      },
      // Edit bar configuration
      editorConfig: {
        placeholder: "Please enter relevant content...",
        //Menu configuration
        MENU_CONF: {
          // ===================
          //Upload image configuration
          // ===================
          uploadImage: {
            // file name
            fieldName: "contentAttachImage",
            server: Location.serverPath + "/editor-upload/upload-image",
            headers: {
              Authorization: "Bearer " + getToken(),
            },
            meta: {
              activityKey: this.relationKey,
            },
            //The maximum size limit of a single file, the default is 20M
            maxFileSize: 20 * 1024 * 1024,
            //Maximum number of files can be uploaded, default is 100
            maxNumberOfFiles: 10,
            // Type restriction when selecting files, default is ['image/*']. If you don’t want to limit it, set it to []
            allowedFileTypes: ["image/*"],
            // Whether to pass cookies across domains, the default is false
            withCredentials: true,
            // Timeout, default is 10 seconds
            timeout: 5 * 1000,
            // Customize the insert image operation
            customInsert: (res, insertFn) => {
              if (res.errno == -1) {
                this.$message.error("Upload failed!");
                return;
              }
              insertFn(Location.serverPath + res.data.url, "", "");
              this.$message.success("Upload successful!");
            },
          },
          // =====================
          //Upload video configuration
          // =====================
          uploadVideo: {
            // file name
            fieldName: "contentAttachVideo",
            server: Location.serverPath + "/editor-upload/upload-video",
            headers: {
              Authorization: "Bearer " + getToken(),
            },
            meta: {
              activityKey: this.relationKey,
            },
            //The maximum size limit of a single file, the default is 60M
            maxFileSize: 60 * 1024 * 1024,
            //Maximum number of files can be uploaded, default is 100
            maxNumberOfFiles: 3,
            //Type restriction when selecting files, default is ['video/*']. If you don’t want to limit it, set it to []
            allowedFileTypes: ["video/*"],
            // Whether to pass cookies across domains, the default is false
            withCredentials: true,
            // Timeout, default is 10 seconds
            timeout: 15 * 1000,
            // Customize the insert image operation
            customInsert: (res, insertFn) => {
              if (res.errno == -1) {
                this.$message.error("Upload failed!");
                return;
              }
              insertFn(Location.serverPath + res.data.url, "", "");
              this.$message.success("Upload successful!");
            },
          },
        },
      },

      // ===== data field end =====
    };
  },
  methods: {
    // =============== Editor event related ================
    // 1. Create Editor instance object
    onCreated(editor) {
      this.editor = Object.seal(editor); // Be sure to use Object.seal(), otherwise an error will be reported
      this.$nextTick(() => {
        const toolbar = DomEditor.getToolbar(this.editor);
        const curToolbarConfig = toolbar.getConfig();
        console.log("[ curToolbarConfig 】-39", curToolbarConfig);
      });
    },
    // 2. Lost focus event
    onChange(editor) {
      this.$emit("change", this.html);
    },

    // =============== Editor operation API related ==============
    insertText(insertContent) {
      const editor = this.editor; // Get editor instance
      if (editor == null) {
        return;
      }
      // Execute Editor's API insertion
      editor.insertText(insertContent);
    },

    // =============== Component interaction related ==================
    // closeEditorBeforeComponent() {
    // this.$emit("returnEditorContent", this.html);
    // },
    closeContent(){
        this.html=''
    },
    // ========== methods end ===============
  },
  mounted() {
    // ========== mounted end ===============
  },
  beforeDestroy() {
    const editor = this.editor;
    if (editor == null) {
      return;
    }
    editor.destroy();
    console.log("Destroy editor!");
  },
};
</script>
<style lang="scss" scoped>
//Penetrate the default p tag
::v-deep .editorStyle .w-e-text-container [data-slate-editor] p {
  margin: 0 !important;
}
</style>
<style src="@wangeditor/editor/dist/css/style.css"></style>

4. Format brush function js file

import {
  SlateEditor,
  SlateText,
  SlateElement,
  SlateTransforms,
  DomEditor,
  //Boot,
} from "@wangeditor/editor";
// Boot.registerMenu(menu1Conf);
import { Editor } from "slate";
export default class MyPainter {
  constructor() {
    this.title = "Format Paint"; // Customize menu title
    // Here is the style for setting the format brush. Both images and svg can be used, but please note that the size of the image should be smaller because it needs to be applied to mouse gestures.
    this.iconSvg = ``;
    this.tag = "button"; //Injected menu type
    this.savedMarks = null; //Saved styles
    this.domId = null; //Do you want this or not?
    this.editor = null; //Editor example
    this.parentStyle = null; //Storage parent node style
    this.mark = "";
    this.marksNeedToRemove = []; // When adding marks, which marks need to be removed (mutually exclusive, cannot coexist)
  }
  clickHandler(e) {
    console.log(e, "e"); //Invalid
  }
  //Add or remove mouse events
  addorRemove = (type) => {
    const dom = document.body;
    if (type === "add") {
      dom.addEventListener("mousedown", this.changeMouseDown);
      dom.addEventListener("mouseup", this.changeMouseup);
    } else {
      //Cleanup work that needs to be done after assignment
      this.savedMarks = undefined;
      dom.removeEventListener("mousedown", this.changeMouseDown);
      dom.removeEventListener("mouseup", this.changeMouseup);
      document.querySelector("#w-e-textarea-1").style.cursor = "auto";
    }
  };

  //Handle the situation where duplicate key names have different values
  handlerRepeatandNotStyle = (styles) => {
    const addStyles = styles[0];
    const notVal = [];
    for (const style of styles) {
      for (const key in style) {
        const value = style[key];
        if (!addStyles.hasOwnProperty(key)) {
          addStyles[key] = value;
        } else {
          if (addStyles[key] !== value) {
            notVal.push(key);
          }
        }
      }
    }
    for (const key of notVal) {
      delete addStyles[key];
    }
    return addStyles;
  };

  // Get the parent node of the currently selected range
  getSelectionParentEle = (type, func) => {
    if (this.editor) {
      const parentEntry = SlateEditor.nodes(this.editor, {
        match: (node) => SlateElement.isElement(node),
      });
      let styles = [];
      for (const [node] of parentEntry) {
        styles.push(this.editor.toDOMNode(node).style); //Add the style object corresponding to the DOM corresponding to the node to the array
      }
      styles = styles.map((item) => {
        //Handle styles that are not empty
        const newItem = {};
        for (const key in item) {
          const val = item[key];
          if (val !== "") {
            newItem[key] = val;
          }
        }
        return newItem;
      });
      type === "get"
        ? func(type, this.handlerRepeatandNotStyle(styles))
        : func(type);
    }
  };

  //Get or set the parent style
  getorSetparentStyle = (type, style) => {
    if (type === "get") {
      this.parentStyle = style; //This is a style object such as {textAlign:'center'}
    } else {
      SlateTransforms.setNodes(
        this.editor,
        { ...this.parentStyle },
        {
          mode: "highest", // For the highest level node
        }
      );
    }
  };

  //Here is to convert svg to Base64 format
  addmouseStyle = () => {
    const icon = ``; //Here is to add an icon to the mouse gesture
    // Encode the string into Base64 format
    const base64String = btoa(icon);
    // Generate data URI
    const dataUri = `data:image/svg + xml;base64,${base64String}`;
    //Apply data URI to mouse icon
    document.querySelector(
      "#w-e-textarea-1"
    ).style.cursor = `url('${dataUri}'), auto`;
  };
  changeMouseDown = () => {}; //Mouse down

  changeMouseup = () => {
    //Mouse raised
    if (this.editor) {
      const editor = this.editor;
      const selectTxt = editor.getSelectionText(); //Get whether the text is null
      if (this.savedMarks & amp; & amp; selectTxt) {
        //Change the parent node style first
        this.getSelectionParentEle("set", this.getorSetparentStyle);
        // Get all text nodes
        const nodeEntries = SlateEditor.nodes(editor, {
          //nodeEntries returns an iterator object
          match: (n) => SlateText.isText(n), //This is to filter whether a node is text
          universal: true, //When universal is true, Editor.nodes will traverse the entire document, including the root node and all child nodes, to match nodes that meet the conditions. When universal is false , Editor.nodes will only match among the direct children of the current node.
        });
        // First clear the style of the selected node
        for (const node of nodeEntries) {
          const n = node[0]; //{text:xxxx}
          const keys = Object.keys(n);
          keys.forEach((key) => {
            if (key === "text") {
              // keep text attribute
              return;
            }
            //Other attributes, clear all
            SlateEditor.removeMark(editor, key);
          });
        }
        //Assign new style
        for (const key in this.savedMarks) {
          if (Object.hasOwnProperty.call(this.savedMarks, key)) {
            const value = this.savedMarks[key];
            editor.addMark(key, value);
          }
        }
        this.addorRemove("remove");
      }
    }
  };

  getValue(editor) {
    // return "MyPainter"; // Logo format brush menu
    const mark = this.mark;
    console.log(mark, "mark");
    const curMarks = Editor.marks(editor);
    // When curMarks exists, it means that the user manually sets it, whichever is curMarks.
    if (curMarks) {
      return curMarks[mark];
    } else {
      const [match] = Editor.nodes(editor, {
        // @ts-ignore
        match: (n) => n[mark] === true,
      });
      return !!match;
    }
  }

  isActive(editor, val) {
    const isMark = this.getValue(editor);
    return !!isMark;
    // return !!DomEditor.getSelectedNodeByType(editor, "geshi");
    // return false;
  }

  isDisabled(editor) {
    //Whether to disable
    return false;
  }
  exec(editor, value) {
    //Triggered when the menu is clicked
    // console.log(!this.isActive());
    console.log(value, "value");
    this.editor = editor;
    this.domId = editor.id.split("-")[1]
      ? `w-e-textarea-${editor.id.split("-")[1]}`
      : undefined;
    if (this.isDisabled(editor)) return;
    const { mark, marksNeedToRemove } = this;
    if (value) {
      // Already, cancel
      editor.removeMark(mark);
    } else {
      // If not, execute
      editor.addMark(mark, true);
      this.savedMarks = SlateEditor.marks(editor); // Get the style of the currently selected text
      this.getSelectionParentEle("get", this.getorSetparentStyle); //Get the parent node style and assign it
    // this.addmouseStyle(); //Add style to the mouse after clicking
      this.addorRemove("add"); //Processing add and remove event functions
      // Remove mutually exclusive and incompatible marks
      if (marksNeedToRemove) {
        marksNeedToRemove.forEach((m) => editor.removeMark(m));
      }
    }
    if (
      editor.isEmpty() ||
      editor.getHtml() == "<p><br></p>" ||
      editor.getSelectionText() == ""
    )
      return; //Here is the processing for unselected or no content
  }
}

5. Page application components

 <el-form-item label="content">
 <WangEdit v-model="form.content" ref="wangEdit" @change="change"></WangEdit>
  </el-form-item>


//js
const WangEdit = () => import("@/views/compoments/WangEdit.vue");
export default {
  name: "Notice",
  components: {
    WangEdit,
  },
    data(){
    return{
          form:{
         }
    }
    },

 methods: {
     change(val) {
            console.log(val,'aa');
            this.form.content=val
        },
     // Cancel button
    cancel() {
      this.open = false;
      this.form={};
      this.$refs.wangEdit.closeContent();
    },
}