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
- ButtonMenu: https://www.wangeditor.com/v5/development.html#buttonmenu
- Register menu to wangEditor: Customize new extension features | wangEditor
- insertKeys keys for custom functions: https://www.wangeditor.com/v5/toolbar-config.html#insertkeys
- Custom upload image and video function: menu configuration | wangEditor
- 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(); }, }