wangeditor编辑器自定义按钮和节点,上传word转换html,文本替换

devtools/2024/9/24 3:13:35/

vue3+ts

需求:在编辑器插入图片和视频时下方会有一个输入框填写描述,上传word功能

wangeditor文档wangEditor开源 Web 富文本编辑器,开箱即用,配置简单icon-default.png?t=N7T8https://www.wangeditor.com/

 安装:npm install @wangeditor/editor --save

1、自定义按钮部分 index.ts,参考了文档

import type { IButtonMenu, IDomEditor } from "@wangeditor/editor-for-vue";
import { Range } from "slate";
import { DomEditor } from "@wangeditor/editor";class VideoMenu implements IButtonMenu {title: string;tag: string;iconSvg: string;constructor() {this.title = "上传视频";this.iconSvg ='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="black" d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"/></svg>';this.tag = "button";}getValue() {return " ";}isActive() {return false;}isDisabled(editor: IDomEditor): boolean {//这部分参考的源码写的const { selection } = editor;if (selection == null) return true;if (!Range.isCollapsed(selection)) return true; // 选区非折叠,禁用const selectedElems = DomEditor.getSelectedElems(editor);const hasVoidOrPre = selectedElems.some(elem => {const type = DomEditor.getNodeType(elem);if (type === "pre") return true;if (type === "list-item") return true;if (editor.isVoid(elem)) return true;return false;});if (hasVoidOrPre) return true; // void 或 pre ,禁用return false;}exec(editor: IDomEditor) {if (this.isDisabled(editor)) return;//点击打开上传视频的弹框editor.emit("uploadvideo");}
}
class TextReplace implements IButtonMenu {title: string;iconSvg: string;tag: string;constructor() {this.title = "文本替换";this.iconSvg ='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path fill="black" d="M11 6c1.38 0 2.63.56 3.54 1.46L12 10h6V4l-2.05 2.05A6.976 6.976 0 0 0 11 4c-3.53 0-6.43 2.61-6.92 6H6.1A5 5 0 0 1 11 6m5.64 9.14A6.89 6.89 0 0 0 17.92 12H15.9a5 5 0 0 1-4.9 4c-1.38 0-2.63-.56-3.54-1.46L10 12H4v6l2.05-2.05A6.976 6.976 0 0 0 11 18c1.55 0 2.98-.51 4.14-1.36L20 21.49L21.49 20z"/></svg>';this.tag = "button";}getValue() {return false;}isActive() {return false;}isDisabled(editor: IDomEditor): boolean {const { selection } = editor;if (selection == null) return true;return false;}exec(editor: IDomEditor) {if (this.isDisabled(editor)) return;editor.emit("toggleModal", "textReplace", true);}
}class sendwordMenu implements IButtonMenu {title: string;tag: string;constructor() {this.title = "上传word";this.tag = "button";}getValue() {return " ";}isActive() {return false;}isDisabled(editor: IDomEditor): boolean {const { selection } = editor;if (selection == null) return true;if (!Range.isCollapsed(selection)) return true; // 选区非折叠,禁用const selectedElems = DomEditor.getSelectedElems(editor);const hasVoidOrPre = selectedElems.some(elem => {const type = DomEditor.getNodeType(elem);if (type === "pre") return true;if (type === "list-item") return true;if (editor.isVoid(elem)) return true;return false;});if (hasVoidOrPre) return true; // void 或 pre ,禁用}exec(editor: IDomEditor) {if (this.isDisabled(editor)) return;//这里写点击按钮后的操作,我这里是调自定义事件editor.emit("uploadword");}
}
export const menu1Conf = {key: "videomenu", // 定义 menu key :要保证唯一、不重复(重要)factory() {return new VideoMenu();}
};export const menu2Conf = {key: "wordmenu",factory() {return new sendwordMenu();}
};
export const menu3Conf = {key: "textReplace",factory() {return new TextReplace();}
};

 2、editorComponents.vue代码,在editor组件中引入index.ts和renderviedoEle/index和renderimgEle/index 

<script setup lang="ts">
import {onBeforeUnmount,ref,reactive,shallowRef,defineEmits,defineProps,
} from "vue";
import "@wangeditor/editor/dist/css/style.css";
import {Editor,Toolbar,IDomEditor,
} from "@wangeditor/editor-for-vue";
import {Boot,DomEditor,
} from "@wangeditor/editor";
import type { UploadInstance } from "element-plus";
import mammoth from "mammoth";
import customvideo from "@/utils/renderviedoEle/index";
import customimage from "@/utils/renderimgEle/index";
import {menu1Conf,menu2Conf,menu3Conf,
} from "@/utils/menus/index";
defineOptions({name: "editUpload"
});
const emit = defineEmits(["changevalue",
]);const mode = "default";
const props = defineProps({editvalue: {type: String,default: ""},
});
const localeditvalue = ref(props.editvalue);
const txtplace = reactive({findContent: "",replaceContent: ""
});
const textReplaceShow = ref(false);const replaceTextInHTML = function (html, searchText, replaceText) {// 定义全局匹配的正则表达式,匹配除了HTML标签之外的所有内容const regex = />([^<]*)</g;// 使用replace方法替换匹配到的文本内容const replacedHtml = html.replace(regex, (match, text) => {// 判断文本内容是否包含需要替换的搜索文本if (text.includes(searchText)) {// 替换文本内容const replacedText = text.replace(new RegExp(searchText, "g"),replaceText);return `>${replacedText}<`;} else {// 不需要替换,返回原内容return match;}});return replacedHtml;
};
const handleSubmit = () => {//替换文本提交const html = editorRef.value.getHtml();const newHtml = replaceTextInHTML(html,txtplace.findContent,txtplace.replaceContent);editorRef.value.setHtml(newHtml);
};const insertVideo = val => {//插入视频editorRef.value.restoreSelection();// 恢复选区setTimeout(() => {editorRef.value.insertNode({type: "customvideo",src: val.videoUrl,poster: val.coverUrl,videoId: val.videoID,altDes: "",children: [{text: ""}]});}, 500);
} 
const sendeluploads = ref<UploadInstance>();
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
const toolbarConfig: any = {//这里把不想要的菜单排除掉excludeKeys: ["insertImage","insertVideo","uploadVideo","editvideomenu","group-video"]
};
const editorConfig = {placeholder: "请输入内容...",MENU_CONF: {}
};
// 在工具栏插入自定义的按钮
toolbarConfig.insertKeys = {index: 19, // 插入的位置,基于当前的 toolbarKeyskeys: ["videomenu","wordmenu","textReplace"]
};//注意:这个要再外面注入,不然会报错
Boot.registerModule(customvideo); 
Boot.registerModule(customimage);
const handleCreated = (editor: IDomEditor) => {editorRef.value = editor;// 判断已插入过就不要重复插入按钮if (!editor.getAllMenuKeys()?.includes("videomenu","wordmenu","textReplace")) {Boot.registerMenu(menu1Conf);Boot.registerMenu(menu2Conf);Boot.registerMenu(menu3Conf);}editor.on("uploadvideo", val => {// 处理上传视频的逻辑,上传完直接插入视频 insertVideo()// ........});editor.on("uploadword", () => {// 点击上传word按钮模拟上传事件cliksendeluploads.value.$.vnode.el.querySelector("input").click();});editor.on("toggleModal", (modalName, show) => {// 显示替换的弹框textReplaceShow.value = show;});};
const onChange = editor => {//编辑器的值改变emit("changevalue", editor.getHtml());
};
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {const editor = editorRef.value;if (editor == null) return;editor.destroy();
});// 图片上传阿里云服务器
editorConfig.MENU_CONF["uploadImage"] = {// 自定义上传async customUpload(file: File, insertFn) {aliyunApi(file).then((res: any) => {// 上传到服务器后插入自定义图片节点editorRef.value.insertNode({type: "customimage",src: res.url,alt: res.name,href: res.url,children: [{text: ""}]});});}
};const handleSuccess = val => {};
const beforeUpload = val => {};
const handleUpload = val => {//上传完word文档后的处理,此处用到了mammoth.js,查看地址:https://github.com/mwilliamson/mammoth.js// word文档转换插入到富文本const file = val.file;var reader = new FileReader();reader.onload = function (loadEvent) {var arrayBuffer = loadEvent.target?.result;mammoth.convertToHtml({ arrayBuffer: arrayBuffer as ArrayBuffer },{ convertImage: convertImage }//将base64图片转换上传到阿里云服务器).then(function (result) {// 没能修改插入图片的源码,这里自己做了下修改,加了customimage的div,让图片渲染走自己定义的节点// 如果没有这一步,会默认插入原先img的那个节点const parser = new DOMParser();const doc = parser.parseFromString(result.value, "text/html");const images = doc.getElementsByTagName("img");for (let i = images.length - 1; i >= 0; i--) {const img = images[i];const div = doc.createElement("div");div.setAttribute("data-w-e-type", "customimage");div.setAttribute("data-w-e-is-void", "");div.setAttribute("data-w-e-is-inline", "");if (img.parentNode) {img.parentNode.replaceChild(div, img);}div.appendChild(img);}const processedHtml = doc.body.innerHTML;editorRef.value.dangerouslyInsertHtml(processedHtml);},function (error) {console.error(error);});};reader.readAsArrayBuffer(file);
};// word图片转换
const convertImage = mammoth.images.imgElement(image => {return image.read("base64").then(async imageBuffer => {const result = await uploadBase64Image(imageBuffer, image.contentType);return { src: result };});
});const uploadBase64Image = async (base64Image, mime) => {const _file = base64ToBlob(base64Image, mime);let data: any = await aliyunApi(_file);return data.url;
};
const base64ToBlob = (base64, mime) => {mime = mime || "";const sliceSize = 1024;const byteChars = window.atob(base64);const byteArrays = [];for (let offset = 0, len = byteChars.length;offset < len;offset += sliceSize) {const slice = byteChars.slice(offset, offset + sliceSize);const byteNumbers = new Array(slice.length);for (let i = 0; i < slice.length; i++) {byteNumbers[i] = slice.charCodeAt(i);}const byteArray = new Uint8Array(byteNumbers);byteArrays.push(byteArray);}return new Blob(byteArrays, { type: mime });
};</script><template><divclass="wangeditor"><Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" /><Editorid="editor-container"v-model="localeditvalue":defaultConfig="editorConfig":mode="mode"style="height: 500px; overflow-y: hidden; border: 1px solid #ccc"@onCreated="handleCreated"@onChange="onChange"/><el-uploadv-show="false"ref="sendeluploads"action="#":show-file-list="false"accept=".docx":on-success="handleSuccess":before-upload="beforeUpload":http-request="handleUpload"/><el-dialogv-model="textReplaceShow"title="文本替换"width="30%"class="replacedialog"><el-formv-model="txtplace"label-width="auto"><el-form-item label="查找文本"><el-input v-model="txtplace.findContent" /></el-form-item><el-form-item label="替换文本"><el-input v-model="txtplace.replaceContent" /></el-form-item><el-form-item><el-button type="primary" @click="handleSubmit">替换</el-button></el-form-item></el-form></el-dialog></div>
</template>
<style scoped lang="scss">
.replacedialog {.el-form {.el-form-item {margin-bottom: 20px;label {font-weight: bold;color: #333;}.el-input {input {color: #333;}}}}
}
</style>
<style lang="scss">
.w-e-image-container {border: 2px solid transparent;
}.w-e-text-container [data-slate-editor] .w-e-selected-image-container {border: 2px solid rgb(180 213 255);
}.w-e-text-container [data-slate-editor] img {display: block !important;margin: 0 auto;
}.w-e-text-container [data-slate-editor] .w-e-image-container {display: block;
}.w-e-text-container [data-slate-editor] .w-e-image-container:hover {box-shadow: none;
}.txt-input {.el-textarea__inner {height: 300px;}
}.w-e-text-container [data-slate-editor] p {margin: 5px 0;
}.w-e-textarea-video-container video {width: 30%;
}.w-e-textarea-video-container {background: none;
}.w-e-text-container[data-slate-editor].w-e-selected-image-container.left-top {display: none;
}.w-e-text-container[data-slate-editor].w-e-selected-image-container.right-top {display: none;
}.w-e-text-container[data-slate-editor].w-e-selected-image-container.left-bottom {display: none;
}.w-e-text-container[data-slate-editor].w-e-selected-image-container.right-bottom {display: none;
}
</style>

 3、在页面中引用editor组件

<script setup lang="ts">
import { ref, reactive } from "vue";
import { EdtiorUpload } from "@/components/editor";
const editorcontent = ref("");
const childeditRef = ref(null);
const editorChange = val => {// 编辑器值改变了...
};
</script><template><div><div style="width: 100%"><!-- 这里组件写ref标识 保证每次组件打开都能更新 --><EdtiorUploadref="childeditRef":editvalue="editorcontent"@changevalue="editorChange"/></div></div>
</template>

4.自定义节点的部分renderviedoEle/index,renderimgEle/index 放在了githubhttps://github.com/srttina/wangeditor-customsalte/tree/master

 


http://www.ppmy.cn/devtools/98208.html

相关文章

MVC 参考手册

MVC 参考手册 1. 引言 MVC(Model-View-Controller)是一种广泛使用的软件架构模式,它将应用程序的逻辑分为三个相互关联的组件:模型(Model)、视图(View)和控制器(Controller)。这种模式最早在20世纪70年代被提出,用于Smalltalk编程语言中,后来被广泛采用于各种编程…

tcpdump的使用

tcpdump是linux上的抓包工具,类似windows系统的wireshark,是最广泛使用的抓包工具 各选项使用方法 -X 最重要的选项 对于tcp包 一定要加上这个才能查看包的内容,知道加这个,等于会用tcpdump了 -i 指定网卡 默认为eth0 但是多网卡时别忘了指定,还有本机调用时走的是lo,不是eth…

设计模式-备忘录模式

概述 备忘录模式也是一种行为型的设计模式&#xff0c;其主要的功能是存储和撤销的功能&#xff0c;可以恢复之前的状态&#xff0c;在实际的开发中&#xff0c;几乎是必不可少的功能&#xff0c;现在几乎所有的软件都少不了撤销的功能&#xff0c;如果没有撤销&#xff0c;那…

SOL项目开发代币DApp的基本要求、模式创建与海外宣发策略

Solana&#xff08;SOL&#xff09;作为一个高性能区块链平台&#xff0c;以其快速的交易速度和低交易成本吸引了大量开发者和投资者。基于Solana开发的去中心化应用程序&#xff08;DApp&#xff09;和代币项目正逐步成为区块链领域的重要组成部分。要成功开发并推广一个SOL项…

Java二十三种设计模式-命令模式(18/23)

命令模式&#xff1a;将请求封装为对象的策略 概要 本文全面探讨了命令模式&#xff0c;从基础概念到实现细节&#xff0c;再到使用场景、优缺点分析&#xff0c;以及与其他设计模式的比较&#xff0c;并提供了最佳实践和替代方案&#xff0c;旨在帮助读者深入理解命令模式并…

前端打字效果

页面效果链接&#xff0c;点击查看https://live.csdn.net/v/419208?spm1001.2014.3001.5501 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, …

字符串String概述,遍历字符串

String的注意点 字符串的内容是不会发生改变的&#xff0c;它的对象在创建后不能被更改 string是Java定义好的一个类&#xff0c;定义在java.long包中&#xff0c;所以使用的时候不需要导入包。 Java程序中的所有字符串文字&#xff08;例如“abcdefg”&#xff09;&#xf…

基于Springboot宠物商城网站系统--论文pf

TOC springboot508基于Springboot宠物商城网站系统--论文pf 第1章 绪论 1.1 课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们…