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

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.rhkb.cn/news/406794.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Datawhale X 李宏毅苹果书 AI夏令营-深度学习入门班-task1

机器学习就是去拟合一种函数&#xff0c;它可能在高维上&#xff0c;十分抽象&#xff0c;但是却可以有丰富的语义含义&#xff0c;从而完成一系列任务 回归任务是预测一个准确的值&#xff0c;例如拟合一条直线的时候&#xff0c;我们希望每一个点的值都能对应上 分类任务则…

java多线程(六)关键字Volatile可见性、有序性以及单个变量的原子性

volatile关键字 作用 volatile 是 Java 虚拟机提供的轻量级的同步机制&#xff0c;主要用来确保变量被线程安全地读取和写入。 当一个变量定义为 volatile 后&#xff0c;它具备以下特性&#xff1a; 可见性&#xff1a;确保不同线程对这个变量操作的可见性&#xff0c;即一…

深入学习SQL优化的第三天

目录 聚合函数 排序和分组 聚合函数 1251. 平均售价 表&#xff1a;Prices------------------------ | Column Name | Type | ------------------------ | product_id | int | | start_date | date | | end_date | date | | price | int …

K8S - Java微服务配置 - 使用ConfigMap配置redis

参考文档&#xff1a;https://v1-27.docs.kubernetes.io/zh-cn/docs/tutorials/configuration/configure-redis-using-configmap/ cat <<EOF >./example-redis-config.yaml apiVersion: v1 kind: ConfigMap metadata:name: example-redis-config data:redis-config: …

“解决Windows电脑无法投影到其他屏幕的问题:尝试更新驱动程序或更换视频卡“

目录 背景: 解决方法1: 解决方法2: 什么是驱动程序&#xff1a; 背景: 今天在日常的工作中&#xff0c; 我想将笔记本分屏到另一个显示屏&#xff0c;我这电脑Windows10系统&#xff0c;当我按下Windows键P键&#xff0c;屏幕信息上提示我"你的电脑不能投影到其他屏幕…

C++:继承(protected、隐藏、不能被继承的类、)

目录 继承的概念 继承的使用 继承方式 protected 继承类模板 赋值兼容转换 隐藏 子类的默认成员函数 构造函数 拷贝构造函数 赋值重载函数 析构函数 不能被继承的类 方法1&#xff1a;父类的构造函数私有 方法2&#xff1a;final 继承与友元 继承与静态成员 …

Redis 键值型数据库

一、Redis是什么 Redis&#xff1a;REmote DIctionary Server&#xff08;远程字典服务器&#xff09; 是完全开源免费的&#xff0c;用C语言编写的&#xff0c;遵守BSD协议&#xff0c;是一个高性能的&#xff08;Key/Value&#xff09;分布式内存数据 库&#xff0c;基于内存…

tcp 和udp通信

一.recvfrom recvfrom函数是一个系统调用&#xff0c;用于从套接字接收数据。该函数通常与无连接的数据报服务&#xff08;如 UDP&#xff09;一起使用&#xff0c;但也可以与其他类型的套接字使用。与简单的 recv() 函数不同&#xff0c;recvfrom() 可以返回数据来源的地址信息…

FreeRTOS学习:内存管理

FreeRTOS内存管理简介 在使用 FreeRTOS 创建任务、队列、信号量等对象的时候&#xff0c; FreeRTOS 一般都提供了两种方法&#xff0c; 动态方法创建&#xff1a;自动地从 FreeRTOS 管理的内存堆中申请所创建对象所需的内存&#xff0c;在对象被删除后&#xff0c;又可以将这…

Vue3开始

1.创建 Vue3 工程 1.基于 vue-cli创建 2.基于vite创建工程 Home | Vite中文网 (vitejs.cn) nodejs 环境可以 使用 nvm 来管理 nodejs 的版本 3.开始创建 4.目录结构 5.创建完 执行 安装依赖 npm i 6.运行项目 为什么命令是 dev 就是从这里配置的 npm run dev

大白话社融中M1和M2的学习笔记

一、背景 为什么我看社融数据呢&#xff1f;因为作为一个码农&#xff0c;我不清楚当前个人所处的样本情况是否真实。我所处的环境是在深圳南山&#xff0c;身边一些同事有不少找工作不怎么顺利&#xff0c;我所在的公司今年也没招大学生了&#xff0c;人员也只出不进为主&…

本地生活服务商系统如何利用本地推获得更多曝光?

随着本地生活赛道中的竞争愈演愈烈&#xff0c;越来越多的本地生活服务商和本地生活商家开始计划着通过在本地推等平台投放相关信息&#xff0c;以提高品牌店铺的曝光量和知名度。不过&#xff0c;就目前的情况来看&#xff0c;绝大多数人都陷入了一种“投入多&#xff0c;转化…

教育部-华为产学合作协同育人项目 | 仓颉编程语言专项

为响应《教育部高等教育司关于调整产学合作协同育人项目运行模式及征集2024年产学合作协同育人项目的通知》号召&#xff0c;华为公司2024年第二批70个项目已发布&#xff0c;其中仓颉编程语言领域共计10个项目&#xff0c;如下所示&#xff0c;通过新工科建设项目&#xff0c;…

U盘安装Ubuntu24.04,乌邦图,UltralISO

文章目录 前言通过UltraISO&#xff0c;制作启动U盘下载镜像制作工具UltraISO(软碟通)下载ubuntu镜像文件制作启动U盘 安装ubuntu设置root密码&#xff0c;并登陆root 前言 在Ubuntu作为主流的linux系统&#xff0c;有时候使用VMware安装使用&#xff0c;总归有一定的性能损耗…

力扣每日一题 数组最后一个元素的最小值 位运算

Problem: 3133. 数组最后一个元素的最小值 &#x1f468;‍&#x1f3eb; 灵神题解 class Solution {public long minEnd(int n, int x) {n--; // 先把 n 减一&#xff0c;这样下面讨论的 n 就是原来的 n-1long ans x;int i 0;int j 0;while((n >> j) > 0){// …

springboot集成海康sdk,针对视频流获取某一点的实时温度

直接上代码吧: 前端页面专递点的x和y的坐标及其设备的ip @RequestMapping(value = "/getRealTemperatureByPoint") public float getRealTemperatureByPoint(HttpServletRequest request) {Map<String, Object> params = ParamUtil.getParams(request);Strin…

小区社区超市商城停车场管理系统-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…

Linux 信号 signal,sigaction,sigqueue,kill,相关函数

驱动与应用的结合&#xff0c;参考我的这篇&#xff1a;https://blog.csdn.net/rjszcb/article/details/113573517 一、 什么是信号 信号是软中断,用于通知进程某个事件已经发生。进程可以选择如何响应信号:忽略、默认处理、自定义处理等。 常见信号有:SIGINT(键盘中断)、SIGK…

C语言程序设计-练习篇

华夏波澜壮阔&#xff0c;少年仍需前行。 十&#xff0c;实现一个函数&#xff0c;打印乘法口诀表&#xff0c;口诀表的行数和列数自己指定 #include <stdio.h> //实现一个函数&#xff0c;打印乘法口诀表&#xff0c;口诀表的行数和列数自己指定 void print_table(int …

启动docker镜像

1、运行容器 2、当前运行的进程 3、当前位置和启动时间 4、cat/etc/redhat-release查看版本 5.镜像是模版&#xff0c;容器是实例 6.容器中没有命令运 7.容器总是能轻易获取 8.配置yum 9.安装http 10.修改index⽂件 11.httpd -k start 12.访问 13.退出就没有服务了 14…